Skip to main content

gam_math/
probability.rs

1use libm::erfc;
2use statrs::function::beta::inv_beta_reg;
3
4const INV_SQRT_PI: f64 = 0.564_189_583_547_756_3;
5const SQRT_2_OVER_PI: f64 = 0.797_884_560_802_865_4;
6
7/// Quantile (inverse CDF) of a Beta distribution with shape parameters `a > 0`
8/// and `b > 0` at probability `p`: the value `x in [0, 1]` with
9/// `I_x(a, b) = p`, where `I` is the regularized incomplete beta.
10///
11/// `p <= 0` maps to the support floor and `p >= 1` to the support ceiling. A
12/// non-finite or non-positive shape yields `NaN`.
13pub fn beta_quantile(p: f64, a: f64, b: f64) -> f64 {
14    if !(a.is_finite() && a > 0.0 && b.is_finite() && b > 0.0) {
15        return f64::NAN;
16    }
17    if !p.is_finite() || p <= 0.0 {
18        return 0.0;
19    }
20    if p >= 1.0 {
21        return 1.0;
22    }
23    inv_beta_reg(a, b, p)
24}
25
26/// The part of `x·x` that `f64` cannot hold: `x² = x*x + square_residual(x)`,
27/// exactly, for every `x` whose square neither overflows nor goes subnormal.
28///
29/// This exists because of what `exp` does to a squared argument. Rounding
30/// `x*x` perturbs it by at most `ulp(x²)/2` — a RELATIVE perturbation of
31/// `ε/2`, which is unremarkable on its own. But `exp` converts a relative
32/// perturbation `δ` of its ARGUMENT into a relative perturbation `x²·δ` of
33/// its RESULT, so `exp(x*x)` carries `x²·ε/2` relative error: `3.7e-14` at
34/// `x = 26`, and `7.7e-14` at the `x ≈ 37` where `φ(x)` finally underflows.
35/// That is two orders worse than the `exp` evaluation's own rounding, and it
36/// is the error `erfcx` and `normal_pdf` were both actually delivering.
37///
38/// The residual is the whole of that discarded term and is itself exactly
39/// representable (Dekker's two-product theorem, in its one-FMA form), so
40/// `exp(x²) = exp(x*x)·exp(residual)` and `exp(residual) = 1 + residual` to
41/// `O(residual²)` — below `1e-27` over the entire domain either caller uses.
42/// One multiply by `1 + residual` therefore buys back every digit, and the
43/// callers below apply it fused so the correction itself costs one more
44/// rounding and nothing else.
45///
46/// `mul_add` is a single instruction wherever FMA is in the baseline ISA
47/// (aarch64, and x86-64 built with `+fma`); on a baseline x86-64 build it is
48/// a `glibc` call, measured at ~2.5 ns. Against `erfcx`'s 38 ns that is 9%;
49/// against `normal_pdf`'s 6.2 ns it is 40% of a function that is nowhere the
50/// bottleneck of a row loop that also assembles a design row and a Hessian
51/// block. Both callers guard the pathological arguments BEFORE calling this,
52/// so it never has to defend `±∞` (whose residual would be `NaN`).
53#[inline]
54fn square_residual(x: f64, rounded_square: f64) -> f64 {
55    x.mul_add(x, -rounded_square)
56}
57
58/// Standard normal PDF phi(x).
59///
60/// The squared argument is carried exactly (see [`square_residual`]); without
61/// that, `exp(-½·fl(x*x))` degrades like `x²·ε/2` and reaches `5.7e-14`
62/// relative before `φ` underflows, against the `3.3e-16` it holds with.
63#[inline]
64pub fn normal_pdf(x: f64) -> f64 {
65    const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
66    let rounded_square = x * x;
67    let head = INV_SQRT_2PI * (-0.5 * rounded_square).exp();
68    if head == 0.0 || head.is_nan() {
69        // The pdf underflowed or `x` was `±∞` (head `0`), or `x` was `NaN`.
70        // Neither admits a relative correction, and `±∞` would feed the
71        // residual an `∞ − ∞`; return the limit the plain form gives.
72        return head;
73    }
74    let residual = square_residual(x, rounded_square);
75    head.mul_add(-0.5 * residual, head)
76}
77
78/// Standard normal CDF Phi(x) evaluated via the exact special-function identity
79///
80///   Phi(x) = 0.5 * erfc(-x / sqrt(2)).
81///
82/// This is the exact Gaussian CDF semantics used throughout the codebase. The
83/// numerical `erfc` implementation may use internal approximations, but the
84/// returned function is the standard normal CDF itself rather than a separate
85/// polynomial surrogate surface.
86#[inline]
87pub fn normal_cdf(x: f64) -> f64 {
88    0.5 * erfc(-x / std::f64::consts::SQRT_2)
89}
90
91/// Scaled complementary error function `erfcx(x) = exp(x²) · erfc(x)`,
92/// specialized to the closed domain `x ∈ [0, +∞]`.
93///
94/// `+∞` maps to the exact limiting value `0`; `NaN` and negative inputs map to
95/// `NaN` because they violate this restricted kernel's domain. For
96/// `0 ≤ x < 26` the direct `exp(x²)·erfc(x)` form is finite. Beyond that point
97/// a six-correction asymptotic expansion avoids overflow while retaining the
98/// representable subnormal tail. At the switch, the first omitted term is
99/// below `2e-17` relative to the leading term.
100///
101/// The direct branch carries `x²` exactly (see [`square_residual`]). Without
102/// that correction the branch degraded like `x²·ε/2` — `1.4e-14` at `x = 10`,
103/// `5.7e-14` by the top of its range — while the asymptotic branch that takes
104/// over at `26` was already delivering `3e-16`. The seam was therefore a
105/// 190-fold step DOWN in error at the point where the code switches to what
106/// reads like the fallback, and the whole `[0, 26)` interval, where every
107/// probit / Mills / log-CDF consumer actually lives, was the inaccurate side.
108/// Both branches now hold `< 5e-16`, so the crossover is invisible.
109#[inline]
110pub fn erfcx_nonnegative(x: f64) -> f64 {
111    if x.is_nan() || x < 0.0 {
112        return f64::NAN;
113    }
114    if x == f64::INFINITY {
115        return 0.0;
116    }
117    if x < 26.0 {
118        // `x` is finite and in `[0, 26)`, so the square is exact-splittable and
119        // `head` is finite and strictly positive (`erfc(26⁻) ≈ 1e-295`).
120        let rounded_square = x * x;
121        let head = rounded_square.exp() * erfc(x);
122        head.mul_add(square_residual(x, rounded_square), head)
123    } else {
124        let inv = 1.0 / x;
125        let inv2 = inv * inv;
126        // erfcx(x) ~ 1/(sqrt(pi)x) * sum_n (-1)^n (2n-1)!!/(2x^2)^n.
127        // Horner form keeps the correction well scaled when `inv2` is tiny.
128        let poly = 1.0
129            + inv2
130                * (-0.5
131                    + inv2
132                        * (0.75
133                            + inv2
134                                * (-1.875
135                                    + inv2 * (6.5625 + inv2 * (-29.53125 + inv2 * 162.421875)))));
136        inv * poly * INV_SQRT_PI
137    }
138}
139
140/// Computes `log(1 - exp(-a))` for `a >= 0` without cancellation.
141#[inline]
142pub fn log1mexp_positive(a: f64) -> f64 {
143    assert!(a >= 0.0, "log1mexp_positive requires a >= 0: a={a}");
144    if a > core::f64::consts::LN_2 {
145        (-(-a).exp()).ln_1p()
146    } else if a > 0.0 {
147        (-(-a).exp_m1()).ln()
148    } else {
149        f64::NEG_INFINITY
150    }
151}
152
153/// Numerically stable signed log-sum-exp.  Given pairs
154/// `(log|aⱼ|, sign(aⱼ))` (with `signs[j] ∈ {−1, 0, +1}`), returns
155/// `(log|S|, sign(S))` for `S = Σⱼ signs[j]·exp(log_mags[j])`.  Positive
156/// and negative magnitudes are reduced separately with the standard
157/// log-sum-exp trick (subtract the max, sum, log, add back); the two
158/// partial sums are then combined via `log(|p − n|) =
159/// max(log p, log n) + log1mexp(|log p − log n|)`, preserving accuracy
160/// even when `p ≈ n` (catastrophic cancellation regime).  When all
161/// signs are zero or all magnitudes are `−∞`, returns
162/// `(NEG_INFINITY, 0.0)`.
163///
164/// A `+∞` log-magnitude denotes an infinite-magnitude term (`exp(+∞) = +∞`)
165/// and dominates the sum: if it appears only with positive sign the result
166/// is `(+∞, +1)`; only with negative sign, `(+∞, −1)` (a log-magnitude of
167/// `+∞` with sign `−1` encodes the value `−∞`); with both signs the sum is
168/// the indeterminate `+∞ − ∞`, returned as `(NaN, 0.0)`.  A `−∞`
169/// log-magnitude is `exp(−∞) = 0` and is correctly dropped.
170pub fn signed_log_sum_exp(log_mags: &[f64], signs: &[f64]) -> (f64, f64) {
171    // Infinite-magnitude terms dominate any finite contribution, so resolve
172    // them before the finite log-sum-exp reduction below. `−∞` log-magnitudes
173    // are `exp(−∞) = 0` and need no special handling.
174    let mut has_pos_inf = false;
175    let mut has_neg_inf = false;
176    for (idx, &lm) in log_mags.iter().enumerate() {
177        if lm == f64::INFINITY {
178            if signs[idx] > 0.0 {
179                has_pos_inf = true;
180            } else if signs[idx] < 0.0 {
181                has_neg_inf = true;
182            }
183        }
184    }
185    match (has_pos_inf, has_neg_inf) {
186        // P = +∞, N = +∞ ⇒ indeterminate +∞ − ∞.
187        (true, true) => return (f64::NAN, 0.0),
188        // P = +∞, N < ∞ ⇒ S = +∞.
189        (true, false) => return (f64::INFINITY, 1.0),
190        // N = +∞, P < ∞ ⇒ S = −∞, encoded as log-magnitude +∞ with sign −1.
191        (false, true) => return (f64::INFINITY, -1.0),
192        (false, false) => {}
193    }
194
195    let mut pos_max = f64::NEG_INFINITY;
196    let mut neg_max = f64::NEG_INFINITY;
197    for (idx, &lm) in log_mags.iter().enumerate() {
198        if signs[idx] > 0.0 {
199            pos_max = pos_max.max(lm);
200        } else if signs[idx] < 0.0 {
201            neg_max = neg_max.max(lm);
202        }
203    }
204
205    let mut pos_sum = 0.0_f64;
206    let mut neg_sum = 0.0_f64;
207    for (idx, &lm) in log_mags.iter().enumerate() {
208        if !lm.is_finite() {
209            continue;
210        }
211        if signs[idx] > 0.0 {
212            pos_sum += (lm - pos_max).exp();
213        } else if signs[idx] < 0.0 {
214            neg_sum += (lm - neg_max).exp();
215        }
216    }
217
218    let log_pos = if pos_sum > 0.0 {
219        pos_max + pos_sum.ln()
220    } else {
221        f64::NEG_INFINITY
222    };
223    let log_neg = if neg_sum > 0.0 {
224        neg_max + neg_sum.ln()
225    } else {
226        f64::NEG_INFINITY
227    };
228
229    if log_pos == f64::NEG_INFINITY && log_neg == f64::NEG_INFINITY {
230        // Both partial sums are empty: no terms at all, all signs zero, or every
231        // magnitude `−∞` (each `exp(−∞) = 0`). The signed sum is exactly `0`, so
232        // the contract requires `(−∞, 0.0)` — NOT the positive-sum convention,
233        // which would mislabel a zero as `+1` and corrupt any downstream cascade
234        // that reads back the sign.
235        return (f64::NEG_INFINITY, 0.0);
236    }
237    if log_neg == f64::NEG_INFINITY {
238        return (log_pos, 1.0);
239    }
240    if log_pos == f64::NEG_INFINITY {
241        return (log_neg, -1.0);
242    }
243    if log_pos > log_neg {
244        let gap = log_pos - log_neg;
245        (log_pos + log1mexp_positive(gap), 1.0)
246    } else if log_neg > log_pos {
247        let gap = log_neg - log_pos;
248        (log_neg + log1mexp_positive(gap), -1.0)
249    } else {
250        (f64::NEG_INFINITY, 0.0)
251    }
252}
253
254/// Numerically stable `ln Φ(x)` for the standard normal CDF. For `x ≥ 0`,
255/// evaluates `ln(1 - 0.5 erfc(x/sqrt(2)))` with `ln_1p`, retaining the small
256/// negative result after `Φ(x)` itself rounds to one. For `x < 0`, rewrites
257/// `ln Φ(x) = −u² + ln(½·erfcx(u))`, `u = −x/√2`,
258/// which preserves digits throughout the representable left tail without a
259/// probability floor. Returns the corresponding IEEE limit at infinities and
260/// propagates `NaN`.
261#[inline]
262pub fn normal_logcdf(x: f64) -> f64 {
263    if x == f64::INFINITY {
264        return 0.0;
265    }
266    if x == f64::NEG_INFINITY {
267        return f64::NEG_INFINITY;
268    }
269    if x.is_nan() {
270        return f64::NAN;
271    }
272    if x < 0.0 {
273        let (u, scaled_tail) = negative_normal_tail_components(x);
274        negative_normal_logcdf_from_scaled_tail(u, scaled_tail)
275    } else {
276        let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
277        (-upper_tail).ln_1p()
278    }
279}
280
281/// Numerically stable `ln(1 − Φ(x)) = ln Φ(−x)` for the standard normal
282/// survival function.  Delegates to `normal_logcdf(-x)` so the deep-right
283/// tail benefits from the same `erfcx`-based representation.
284#[inline]
285pub fn normal_logsf(x: f64) -> f64 {
286    normal_logcdf(-x)
287}
288
289/// Joint evaluation of `ln Φ(x)` and the Mills-ratio analogue
290/// `φ(x) / Φ(x)`, signed for the symmetric branch.  Used by the latent
291/// probit families where the inverse-link gradient needs the ratio and
292/// the likelihood needs the log-CDF on the same `x`; computing both in
293/// one call shares the `erfcx` evaluation that dominates the cost in the
294/// deep tail.
295#[inline]
296pub fn signed_probit_logcdf_and_mills_ratio(x: f64) -> (f64, f64) {
297    if x == f64::INFINITY {
298        return (0.0, 0.0);
299    }
300    if x == f64::NEG_INFINITY {
301        return (f64::NEG_INFINITY, f64::INFINITY);
302    }
303    if x.is_nan() {
304        return (f64::NAN, f64::NAN);
305    }
306    if x < 0.0 {
307        let (u, scaled_tail) = negative_normal_tail_components(x);
308        (
309            negative_normal_logcdf_from_scaled_tail(u, scaled_tail),
310            SQRT_2_OVER_PI / scaled_tail,
311        )
312    } else {
313        let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
314        let cdf = 1.0 - upper_tail;
315        let lambda = normal_pdf(x) / cdf;
316        ((-upper_tail).ln_1p(), lambda)
317    }
318}
319
320#[inline]
321fn negative_normal_tail_components(x: f64) -> (f64, f64) {
322    assert!(x.is_finite() && x < 0.0);
323    let u = -x / std::f64::consts::SQRT_2;
324    (u, erfcx_nonnegative(u))
325}
326
327#[inline]
328fn negative_normal_logcdf_from_scaled_tail(u: f64, scaled_tail: f64) -> f64 {
329    -u * u + scaled_tail.ln() - std::f64::consts::LN_2
330}
331
332/// Stable value and first four derivatives of `ln Φ(x)`.
333///
334/// The moderate regime uses the exact Mills-ratio recurrence, with the brackets
335/// collected in `q = λ + x` once `x < 0` so that they do not cancel as `λ`
336/// closes on `−x`. In the deep left tail, differentiating the Laplace continued
337/// fraction
338///
339/// `φ(t)/Φ(-t) = t + 1/(t + 2/(t + 3/(...)))`, `t = -x`,
340///
341/// carries the small correction to `t` independently, so `f'' -> -1` and the
342/// higher derivatives approach zero without subtracting nearly equal `f64`s.
343/// In the right tail, signed log-magnitude sums preserve polynomially weighted
344/// derivatives even when `φ(x)/Φ(x)` itself has rounded to zero.
345#[inline]
346pub fn normal_logcdf_derivatives(x: f64) -> [f64; 5] {
347    if x.is_nan() {
348        return [f64::NAN; 5];
349    }
350    if x == f64::INFINITY {
351        return [0.0; 5];
352    }
353    if x == f64::NEG_INFINITY {
354        return [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0];
355    }
356
357    const RIGHT_LOG_MAGNITUDE_SWITCH: f64 = 8.0;
358    if x <= LEFT_CONTINUED_FRACTION_SWITCH {
359        return normal_logcdf_derivatives_left_tail(x);
360    }
361    if x >= RIGHT_LOG_MAGNITUDE_SWITCH {
362        return normal_logcdf_derivatives_right_tail(x);
363    }
364
365    let (log_cdf, lambda) = signed_probit_logcdf_and_mills_ratio(x);
366    let x2 = x * x;
367    if x < 0.0 {
368        // Left of the origin the brackets below are collected in the SAME Mills
369        // correction `q = λ + x` the continued-fraction branch carries, because
370        // written in `λ` they cancel catastrophically long before the branch
371        // ends. `λ(x) → −x` as `x → −∞`, so every term of, say,
372        // `(x³−3x) + (7x²−4)λ + 12xλ² + 6λ³` grows like `|x|³` while their sum
373        // decays: at `x = −4` they are `−52`, `456`, `−857`, `453` and add to
374        // `−0.0023`, a cancellation of 380000 that costs eleven digits. In `q`
375        // the same bracket is `−6q³ + 6xq² + (4−x²)q − x`, whose terms are
376        // `−0.069`, `−1.22`, `−2.71`, `4` — a cancellation of 1847, three
377        // orders milder. The reformulation is exact (`λ = q − x` substituted and
378        // re-collected), costs the same flops, and buys 16–34x across the whole
379        // branch: worst over `x ∈ [−4, 0]` falls from `4.5e−11` to `2.8e−12`.
380        //
381        // `q` itself is safe to form here: `λ/2 ≤ |x| ≤ 2λ` holds over most of
382        // the range, so `λ + x` is EXACT by Sterbenz, and where it is not (`x`
383        // near 0) `q` is the same size as `λ` and nothing cancels. That is the
384        // whole reason the rewrite works — it moves the cancellation out of the
385        // brackets and into a subtraction that has none.
386        //
387        // Past the origin `q → x` is no longer small, the `λ` form has nothing
388        // to cancel (`λ → 0` and `x² − 1` dominates), and it is the more
389        // accurate of the two — hence the sign test rather than a blanket swap.
390        let q = lambda + x;
391        let q2 = q * q;
392        return [
393            log_cdf,
394            lambda,
395            -lambda * q,
396            lambda * (2.0 * q2 - x * q - 1.0),
397            lambda * (-6.0 * q2 * q + 6.0 * x * q2 + (4.0 - x2) * q - x),
398        ];
399    }
400    let lambda2 = lambda * lambda;
401    let lambda3 = lambda2 * lambda;
402    [
403        log_cdf,
404        lambda,
405        -lambda * (x + lambda),
406        lambda * (x2 - 1.0 + 3.0 * x * lambda + 2.0 * lambda2),
407        -lambda
408            * ((x * x2 - 3.0 * x) + (7.0 * x2 - 4.0) * lambda + 12.0 * x * lambda2 + 6.0 * lambda3),
409    ]
410}
411
412#[derive(Clone, Copy)]
413struct MillsCorrectionDerivatives {
414    value: f64,
415    first: f64,
416    second: f64,
417    third: f64,
418}
419
420/// `x` at or below which the left-tail Mills ratio is taken from the Laplace
421/// continued fraction rather than from `erfcx`. Equivalently `t = −x ≥ 4`.
422const LEFT_CONTINUED_FRACTION_SWITCH: f64 = -4.0;
423
424/// The Laplace continued-fraction **correction** to the left-tail Mills ratio,
425///
426/// `q(t) = λ(−t) − t = 1/(t + 2/(t + 3/(...)))`,   `λ(x) = φ(x)/Φ(x)`,
427///
428/// together with its first three derivatives in `t`. Requires `t ≥ 4`.
429///
430/// `q` is the whole content of the left tail that is NOT the leading `t`: it
431/// decays like `1/t − 2/t³ + 10/t⁵ − ...`, and every operation building it is
432/// a division or an addition of positive quantities, so it carries full
433/// relative precision no matter how small it gets. That is the property its
434/// two consumers need, and it is why the correction is returned separately
435/// instead of pre-added to `t`:
436///
437/// * [`normal_logcdf_derivatives_left_tail`] needs `f'' = −(1 + q')` and the
438///   higher derivatives, which tend to `−1` and `0` and would be destroyed by
439///   differencing nearly equal `f64`s.
440/// * [`cone_boundary_log_factor_and_derivatives`] needs `∂corr/∂a = b − q(t)`,
441///   which is the same statement one substitution away (#2306 §4).
442///
443/// Recovering `q` from a separately computed `λ` — `q = λ − t` — is exactly the
444/// cancellation this exists to avoid, and it is not a small effect: at `t = 1e8`
445/// it costs every significant digit, and past `t ≈ 2e8` it returns the wrong
446/// SIGN. The reference itself has to be carried at ~120 decimal digits before it
447/// reproduces what this recursion gives in binary64.
448#[inline]
449fn mills_correction_continued_fraction(t: f64) -> MillsCorrectionDerivatives {
450    assert!(t.is_finite() && t >= 4.0);
451    let mut q = MillsCorrectionDerivatives {
452        value: 0.0,
453        first: 0.0,
454        second: 0.0,
455        third: 0.0,
456    };
457    // The truncation error is damped by a product of the continued-fraction
458    // sensitivities `n/(t + q)^2`, so the depth must be sized at `t = 4` — the
459    // LEAST converged point of the domain, and the one the log-CDF branch sits
460    // exactly on. Each successive derivative converges roughly 15x slower than
461    // the last, because differentiating the recursion multiplies each level's
462    // contribution by another factor of that same sensitivity. Measured against
463    // a 60-digit reference at `t = 4`:
464    //
465    // ```text
466    //            q         q'        q''       q'''
467    //   32   1.9e-15    7.0e-14    1.4e-12    2.1e-11
468    //   64   2.3e-23    1.4e-21    4.4e-20    1.0e-18
469    // ```
470    //
471    // 32 levels is enough for the VALUE and nothing else: it leaves `q'''` — the
472    // fourth log-CDF derivative — wrong in its eleventh digit. The depths that
473    // first reach `1e-17` at `t = 4` are 41, 47, 53 and 60 for the four
474    // channels, so 64 covers the worst of them with ~200x of margin, and the
475    // requirement falls off fast enough (33 levels at `t = 6`, 24 at `t = 8`,
476    // 12 at `t = 20`) that one constant sized for the edge is safe everywhere
477    // above it. The extra levels are pure convergence — every step divides
478    // positive quantities — so they cannot destabilise a large `t`.
479    for n in (1..=64).rev() {
480        let denominator = t + q.value;
481        let inv_denominator = denominator.recip();
482        let value = f64::from(n) / denominator;
483        let denominator_first = 1.0 + q.first;
484        let a = denominator_first * inv_denominator;
485        let b = q.second * inv_denominator;
486        let c = q.third * inv_denominator;
487        q = MillsCorrectionDerivatives {
488            value,
489            first: -value * denominator_first / denominator,
490            second: value * (2.0 * a * a - b),
491            third: value * (-6.0 * a * a * a + 6.0 * a * b - c),
492        };
493    }
494    q
495}
496
497#[inline]
498fn normal_logcdf_derivatives_left_tail(x: f64) -> [f64; 5] {
499    assert!(x.is_finite() && x <= LEFT_CONTINUED_FRACTION_SWITCH);
500    let t = -x;
501    let q = mills_correction_continued_fraction(t);
502    [
503        normal_logcdf(x),
504        t + q.value,
505        -(1.0 + q.first),
506        q.second,
507        -q.third,
508    ]
509}
510
511#[inline]
512fn normal_logcdf_derivatives_right_tail(x: f64) -> [f64; 5] {
513    assert!(x.is_finite() && x >= 8.0);
514    const LOG_SQRT_2PI: f64 = 0.918_938_533_204_672_7;
515    let log_cdf = normal_logcdf(x);
516    let u = x / std::f64::consts::SQRT_2;
517    let log_lambda = -u * u - LOG_SQRT_2PI - log_cdf;
518    let log_x = x.ln();
519    let inv_x2 = x.recip() * x.recip();
520
521    let first = log_lambda.exp();
522    let second = signed_exp_sum(&[log_x + log_lambda, 2.0 * log_lambda], &[-1.0, -1.0]);
523    let third = signed_exp_sum(
524        &[
525            2.0 * log_x + (-inv_x2).ln_1p() + log_lambda,
526            3.0_f64.ln() + log_x + 2.0 * log_lambda,
527            2.0_f64.ln() + 3.0 * log_lambda,
528        ],
529        &[1.0, 1.0, 1.0],
530    );
531    let fourth = signed_exp_sum(
532        &[
533            3.0 * log_x + (-3.0 * inv_x2).ln_1p() + log_lambda,
534            7.0_f64.ln() + 2.0 * log_x + (-(4.0 / 7.0) * inv_x2).ln_1p() + 2.0 * log_lambda,
535            12.0_f64.ln() + log_x + 3.0 * log_lambda,
536            6.0_f64.ln() + 4.0 * log_lambda,
537        ],
538        &[-1.0, -1.0, -1.0, -1.0],
539    );
540    [log_cdf, first, second, third, fourth]
541}
542
543#[inline]
544fn signed_exp_sum(log_magnitudes: &[f64], signs: &[f64]) -> f64 {
545    let (log_magnitude, sign) = signed_log_sum_exp(log_magnitudes, signs);
546    if sign == 0.0 {
547        0.0
548    } else {
549        sign * log_magnitude.exp()
550    }
551}
552
553#[inline]
554fn acklam_lower_tail_quantile_from_log_probability(log_p: f64) -> f64 {
555    const C: [f64; 6] = [
556        -7.784_894_002_430_293e-3,
557        -3.223_964_580_411_365e-1,
558        -2.400_758_277_161_838,
559        -2.549_732_539_343_734,
560        4.374_664_141_464_968,
561        2.938_163_982_698_783,
562    ];
563    const D: [f64; 4] = [
564        7.784_695_709_041_462e-3,
565        3.224_671_290_700_398e-1,
566        2.445_134_137_142_996,
567        3.754_408_661_907_416,
568    ];
569    let q = (-2.0 * log_p).sqrt();
570    (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
571        / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
572}
573
574/// Standard normal quantile Φ⁻¹(p) using Acklam's rational approximation.
575#[inline]
576pub fn standard_normal_quantile(p: f64) -> Result<f64, String> {
577    if !(p.is_finite() && p > 0.0 && p < 1.0) {
578        return Err(format!("normal quantile requires p in (0,1), got {p}"));
579    }
580
581    const A: [f64; 6] = [
582        -3.969_683_028_665_376e1,
583        2.209_460_984_245_205e2,
584        -2.759_285_104_469_687e2,
585        1.383_577_518_672_69e2,
586        -3.066_479_806_614_716e1,
587        2.506_628_277_459_239,
588    ];
589    const B: [f64; 5] = [
590        -5.447_609_879_822_406e1,
591        1.615_858_368_580_409e2,
592        -1.556_989_798_598_866e2,
593        6.680_131_188_771_972e1,
594        -1.328_068_155_288_572e1,
595    ];
596    const P_LOW: f64 = 0.02425;
597    const P_HIGH: f64 = 1.0 - P_LOW;
598
599    let mut x = if p < P_LOW {
600        acklam_lower_tail_quantile_from_log_probability(p.ln())
601    } else if p <= P_HIGH {
602        let q = p - 0.5;
603        let r = q * q;
604        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
605            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
606    } else {
607        -acklam_lower_tail_quantile_from_log_probability((1.0 - p).ln())
608    };
609    for _ in 0..2 {
610        let density = normal_pdf(x);
611        if !(density.is_finite() && density > 0.0) {
612            break;
613        }
614        // Residual F(x) − p, formed without catastrophic cancellation in
615        // either tail. For an upper-tail iterate `x > 0`, `normal_cdf(x)`
616        // saturates to ~1, so the direct `normal_cdf(x) − p` annihilates the
617        // tiny residual the polish must act on; instead use the upper-tail
618        // complement `F(x) − p = (1 − p) − 0.5·erfc(x/√2)`, where both terms
619        // are the small upper-tail quantities (`1 − p` is exact by Sterbenz
620        // for `p ∈ [½,1)`). For `x ≤ 0`, `normal_cdf(x) = 0.5·erfc(|x|/√2)` is
621        // itself the faithfully carried small lower-tail value, so the direct
622        // form is already cancellation-free.
623        let residual = if x > 0.0 {
624            (1.0 - p) - 0.5 * erfc(x / std::f64::consts::SQRT_2)
625        } else {
626            normal_cdf(x) - p
627        };
628        let correction = residual / density;
629        let denominator = 1.0 + 0.5 * x * correction;
630        if !(correction.is_finite() && denominator.is_finite() && denominator != 0.0) {
631            break;
632        }
633        let step = correction / denominator;
634        if !step.is_finite() {
635            break;
636        }
637        x -= step;
638        if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
639            break;
640        }
641    }
642    Ok(x)
643}
644
645/// Standard normal quantile from `log_p = ln Φ(x)`.
646///
647/// Unlike [`standard_normal_quantile`], this remains defined when `Φ(x)` is
648/// smaller than the least positive `f64`, and when `Φ(x)` is so close to one
649/// that exponentiating `log_p` rounds to exactly one. Acklam's lower-tail
650/// approximation supplies the initial point; Newton polishing solves
651/// `ln Φ(x) = log_p` with the stable log-CDF and Mills ratio, so neither tail
652/// forms a probability-space subtraction.
653#[inline]
654pub fn standard_normal_quantile_from_log_cdf(log_p: f64) -> Result<f64, String> {
655    if !(log_p.is_finite() && log_p < 0.0) {
656        return Err(format!(
657            "normal log-quantile requires finite log_p < 0, got {log_p}"
658        ));
659    }
660
661    if log_p > -std::f64::consts::LN_2 {
662        // Reflect through the upper tail without forming `1 - exp(log_p)`.
663        let log_q = (-log_p.exp_m1()).ln();
664        return standard_normal_quantile_from_log_cdf(log_q).map(|x| -x);
665    }
666
667    let p = log_p.exp();
668    let mut x = if p > 0.0 {
669        standard_normal_quantile(p)?
670    } else {
671        acklam_lower_tail_quantile_from_log_probability(log_p)
672    };
673    for _ in 0..4 {
674        let (current_log_p, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
675        if !(current_log_p.is_finite() && mills_ratio.is_finite() && mills_ratio > 0.0) {
676            break;
677        }
678        let step = (current_log_p - log_p) / mills_ratio;
679        if !step.is_finite() {
680            break;
681        }
682        x -= step;
683        if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
684            break;
685        }
686    }
687    Ok(x)
688}
689
690/// Log of the standardized one-sided truncated-Gaussian boundary factor for
691/// the constrained-LAML cone correction (gam#2306 §4).
692///
693/// For a constraint coordinate with Lagrange multiplier `μ ≥ 0`, normal
694/// curvature `h > 0`, and signed interior slack `s ≥ 0`, the exact 1-D
695/// boundary integral is
696///
697/// ```text
698///   ∫_{−s}^{∞} exp(−μ·u − ½·h·u²) du
699///     = √(2π/h) · exp(μ²/(2h)) · Φ(s·√h − μ/√h),
700/// ```
701///
702/// and the correction of the Laplace criterion RELATIVE to the unrestricted
703/// Gaussian factor `√(2π/h)` is, in the standardized arguments
704/// `a = μ/√h ≥ 0`, `b = s·√h ≥ 0`:
705///
706/// ```text
707///   corr(a, b) = a²/2 + ln Φ(b − a).
708/// ```
709///
710/// Key limits (the #2306 derivation's continuity contract): an activating
711/// row (`a = 0`, `b = 0`) contributes exactly `ln ½` (the half-Gaussian); a
712/// deep interior row (`b − a → ∞`) contributes `→ 0`, reducing byte-exactly
713/// to the unrestricted LAML; a hard-pushed active row (`a → ∞`, `b = 0`)
714/// follows the exact linear-decay limit `corr → −ln(a·√(2π))`.
715///
716/// Evaluated FUSED: computing `a²/2` and `ln Φ(b−a)` as two separate f64
717/// terms cancels catastrophically once `a ≳ 10⁴` (both grow like `±a²/2`).
718/// On the `b < a` branch the sum collapses analytically to
719/// `a·b − b²/2 + ln(erfcx((a−b)/√2)/2)`, which is cancellation-free (for an
720/// active row, `b = 0`, it is a single `erfcx` evaluation).
721#[must_use]
722pub fn cone_boundary_log_factor(mu_over_sqrt_h: f64, slack_times_sqrt_h: f64) -> f64 {
723    let a = mu_over_sqrt_h;
724    let b = slack_times_sqrt_h;
725    if !(a.is_finite() && b.is_finite()) || a < 0.0 || b < 0.0 {
726        return f64::NAN;
727    }
728    let xi = b - a;
729    if xi >= 0.0 {
730        // Interior-dominant: ln Φ(ξ) is a small negative number and a²/2 is
731        // exact; no cancellation between them (a ≤ b here, so a²/2 ≤ ab −
732        // b²/2 + O(1) stays modest whenever the factor itself is modest).
733        0.5 * a * a + normal_logcdf(xi)
734    } else {
735        // Active-dominant: fused analytic collapse of a²/2 + ln Φ(−(a−b)).
736        let u = (a - b) / std::f64::consts::SQRT_2;
737        a * b - 0.5 * b * b + (0.5 * erfcx_nonnegative(u)).ln()
738    }
739}
740
741/// [`cone_boundary_log_factor`] together with its exact partial derivatives
742/// in the standardized arguments — the pieces the outer ρ-gradient chains
743/// through `(μ̃, h̃, s)(ρ)` (gam#2306 §4 "the g-factors differentiate in
744/// closed form"). With `ξ = b − a` and the Mills ratio `λ(ξ) = φ(ξ)/Φ(ξ)`:
745///
746/// ```text
747///   ∂corr/∂a = a − λ(ξ),      ∂corr/∂b = λ(ξ).
748/// ```
749///
750/// `∂corr/∂b = λ(ξ)` is a single `erfcx` evaluation and needs nothing further.
751///
752/// `∂corr/∂a` does. Written literally as `a − λ(ξ)` it is a subtraction of two
753/// quantities that both grow like `a`, because `λ(−t) = t + q(t)` with
754/// `q(t) ~ 1/t`: the answer is the SMALL correction `q`, and forming it by
755/// subtraction destroys `log₁₀(a²·ε)` digits of it. The value
756/// [`cone_boundary_log_factor`] is fused precisely to dodge the twin of this
757/// cancellation, and the gradient has to be fused the same way rather than
758/// re-derived from a `λ` that has already lost the digits.
759///
760/// So on the active branch the correction is taken directly from the Laplace
761/// continued fraction ([`mills_correction_continued_fraction`]), the same one
762/// the left-tail log-CDF derivatives use, under the substitution
763///
764/// ```text
765///   ξ = b − a,  t = −ξ = a − b  ⇒  ∂corr/∂a = a − λ(ξ) = a − (t + q(t)) = b − q(t),
766/// ```
767///
768/// which is cancellation-free for every `a`: `b ≥ 0` and `q(t) ∈ (0, ¼]`. The
769/// deep-active limit `∂corr/∂a → −1/a` then holds to full relative precision
770/// instead of to none, and the sign is right (the factor is strictly decreasing
771/// in `a`, so `∂corr/∂a < 0` whenever `b = 0`).
772///
773/// Measured on `a ∈ [10⁻², 10¹⁴] × b ∈ {0, …, 10³}` against a 250-digit
774/// reference: the subtractive form reaches `6.1e6` relative error and turns
775/// positive past `a ≈ 2e8`; this form is within `7.5e-16` — about 3 ulp — of
776/// the truth, measured against the magnitudes entering the subtraction rather
777/// than against the result. That is the right denominator because `∂corr/∂a`
778/// genuinely passes through ZERO along the curve `b = q(a − b)` (the factor is
779/// increasing in `a` for slack rows and decreasing for active ones), and no
780/// representation carries relative precision across its own root; near it the
781/// error is bounded in absolute terms by `ε·b`, which is what a gradient
782/// consumer needs.
783#[must_use]
784pub fn cone_boundary_log_factor_and_derivatives(
785    mu_over_sqrt_h: f64,
786    slack_times_sqrt_h: f64,
787) -> (f64, f64, f64) {
788    let a = mu_over_sqrt_h;
789    let b = slack_times_sqrt_h;
790    let value = cone_boundary_log_factor(a, b);
791    if value.is_nan() {
792        // The value's domain guard (finite, non-negative `a` and `b`) is the
793        // function's domain; a gradient off it is not defined either, and
794        // returning a finite one next to a NaN value would read as usable.
795        return (value, f64::NAN, f64::NAN);
796    }
797    let xi = b - a;
798    let (_, mills) = signed_probit_logcdf_and_mills_ratio(xi);
799    let d_a = if xi <= LEFT_CONTINUED_FRACTION_SWITCH {
800        b - mills_correction_continued_fraction(-xi).value
801    } else {
802        // `|ξ| < 4`, so `λ(ξ) < λ(−4) ≈ 4.26` and `a = b − ξ` is bounded by it:
803        // the subtraction is between two `O(1)` quantities and loses nothing
804        // that matters.
805        a - mills
806    };
807    (value, d_a, mills)
808}
809
810#[cfg(test)]
811mod cone_boundary_factor_tests {
812    use super::*;
813
814    /// Adaptive-free Simpson quadrature of the exact 1-D boundary integral
815    /// `∫_{−s}^{U} exp(−μu − ½hu²) du` on a truncation `U` chosen so the
816    /// discarded tail is below 1e-18 of the mass.
817    fn quadrature_log_relative_factor(mu: f64, h: f64, s: f64) -> f64 {
818        let upper = ((-mu / h) + 12.0 / h.sqrt()).max(-s + 12.0 / h.sqrt());
819        let lower = -s;
820        let n = 40_000usize;
821        let step = (upper - lower) / n as f64;
822        let f = |u: f64| (-mu * u - 0.5 * h * u * u).exp();
823        let mut acc = f(lower) + f(upper);
824        for i in 1..n {
825            let u = lower + step * i as f64;
826            acc += if i % 2 == 1 { 4.0 } else { 2.0 } * f(u);
827        }
828        let integral = acc * step / 3.0;
829        (integral / (2.0 * std::f64::consts::PI / h).sqrt()).ln()
830    }
831
832    /// The closed form must match direct quadrature of the defining integral
833    /// across active (s=0), interior (μ=0), and mixed regimes (gam#2306 §4).
834    #[test]
835    fn boundary_factor_matches_quadrature_across_regimes() {
836        let cases: [(f64, f64, f64); 8] = [
837            (0.0, 1.0, 0.0),  // activating row: exactly ln ½
838            (0.0, 4.0, 0.0),  // curvature does not move the standardized value
839            (2.5, 1.0, 0.0),  // active with a real multiplier
840            (30.0, 9.0, 0.0), // deep linear-decay limit
841            (0.0, 1.0, 0.7),  // interior near-boundary
842            (0.0, 2.0, 4.0),  // interior far: → 0
843            (1.5, 0.5, 2.0),  // mixed multiplier + slack
844            (4.0, 2.0, 1.0),  // active-dominant mixed
845        ];
846        for &(mu, h, s) in &cases {
847            let a = mu / h.sqrt();
848            let b = s * h.sqrt();
849            let closed = cone_boundary_log_factor(a, b);
850            let quad = quadrature_log_relative_factor(mu, h, s);
851            assert!(
852                (closed - quad).abs() <= 1e-9 * (1.0 + quad.abs()),
853                "(μ={mu}, h={h}, s={s}): closed {closed} vs quadrature {quad}"
854            );
855        }
856        assert!(
857            (cone_boundary_log_factor(0.0, 0.0) - 0.5_f64.ln()).abs() < 1e-15,
858            "an activating row must contribute exactly the half-Gaussian ln ½"
859        );
860    }
861
862    /// The deep-active limit is the exact linear decay `corr → −ln(a·√(2π))`,
863    /// and the deep-interior limit vanishes — the two continuity anchors that
864    /// make the constrained criterion reduce to the unrestricted LAML away
865    /// from the boundary.
866    #[test]
867    fn boundary_factor_limits_are_exact() {
868        let a = 1.0e6;
869        let expected = -(a * (2.0 * std::f64::consts::PI).sqrt()).ln();
870        let got = cone_boundary_log_factor(a, 0.0);
871        assert!(
872            (got - expected).abs() <= 1e-9 * expected.abs(),
873            "deep-active: got {got}, expected {expected}"
874        );
875        let interior = cone_boundary_log_factor(0.0, 40.0);
876        assert!(
877            interior.abs() < 1e-300 || interior > -1e-12,
878            "deep-interior must vanish; got {interior}"
879        );
880    }
881
882    /// The deep-active GRADIENT has to survive as far as the deep-active VALUE
883    /// does. `∂corr/∂a = a − λ(−a)` is the small residual left by two terms
884    /// that both grow like `a`, so writing it as that subtraction loses
885    /// `log₁₀(a²·ε)` digits: at `a = 1e6` it was already 4 digits down, at
886    /// `a = 2e8` it came back POSITIVE, and past `a = 5e8` it was flat zero
887    /// while the true value is `−2e-9`. The value alongside it was correct to
888    /// 15 digits the whole way, which is what made the defect quiet.
889    ///
890    /// The reference here is the asymptotic series of the Mills correction,
891    /// `λ(−a) = a + 1/a − 2/a³ + 10/a⁵ − 74/a⁷ + …` (so `∂corr/∂a = −1/a +
892    /// 2/a³ − …`), which is the cheapest exact statement of the limit and is
893    /// good to well past f64 from `a = 100` up. Finite differences cannot gate
894    /// this: the quantity under test is smaller than any usable FD step's own
895    /// truncation error.
896    #[test]
897    fn boundary_factor_active_gradient_holds_to_the_representable_limit() {
898        let mut a = 100.0_f64;
899        while a <= 1.0e14 {
900            let (_, d_a, _) = cone_boundary_log_factor_and_derivatives(a, 0.0);
901            let inv = 1.0 / a;
902            let expected = -inv + 2.0 * inv.powi(3) - 10.0 * inv.powi(5) + 74.0 * inv.powi(7);
903            assert!(
904                d_a < 0.0,
905                "corr is strictly decreasing in a at b=0, so ∂a must stay negative; \
906                 got {d_a} at a={a}"
907            );
908            assert!(
909                (d_a - expected).abs() <= 1.0e-13 * expected.abs(),
910                "deep-active ∂a at a={a}: got {d_a}, expected {expected} \
911                 (rel {:.3e})",
912                (d_a - expected).abs() / expected.abs()
913            );
914            a *= 10.0;
915        }
916    }
917
918    /// `∂corr/∂a + ∂corr/∂b = a` identically, since the two partials are
919    /// `a − λ(ξ)` and `λ(ξ)` for the same `ξ`. The two are now computed by
920    /// different routes in the active branch — a continued fraction and an
921    /// `erfcx` — so this is the gate that they still describe one function.
922    #[test]
923    fn boundary_factor_partials_sum_to_a() {
924        for &a in &[0.0_f64, 0.5, 3.0, 4.0, 12.0, 1.0e3, 1.0e7, 1.0e12] {
925            for &b in &[0.0_f64, 1.0e-3, 0.9, 5.0, 1.0e3] {
926                let (_, d_a, d_b) = cone_boundary_log_factor_and_derivatives(a, b);
927                assert!(
928                    (d_a + d_b - a).abs() <= 1.0e-14 * a.max(d_b).max(1.0),
929                    "(a={a}, b={b}): ∂a {d_a} + ∂b {d_b} = {} ≠ a",
930                    d_a + d_b
931                );
932            }
933        }
934    }
935
936    /// The continued-fraction branch and the direct `a − λ` form must agree
937    /// just inside the `ξ ≤ −4` switch, where the subtraction still has most of
938    /// its digits. Without this, the branch could be precise and WRONG — the
939    /// accuracy gate above pins a limit the continued fraction could hit while
940    /// disagreeing with the function it is supposed to be differentiating.
941    ///
942    /// The band is set by the instrument being compared against, not by taste.
943    /// `direct` is `a − λ` with `λ` from the `erfcx` route, whose measured
944    /// relative accuracy is `~5e-14` (libm `erfc` plus the `exp(x²)` multiply);
945    /// its absolute error is therefore `~5e-14·λ`, and the subtraction cannot
946    /// remove it. Note how little room that leaves already: the amplification
947    /// `λ/|a−λ|` is 18x at `a = 4` and 403x at `a = 20`, so at the top of this
948    /// range the direct form is down to ~11 correct digits — five short — while
949    /// the continued fraction still matches a 250-digit reference to 16. This
950    /// test is deliberately capped at `a = 20` for that reason; it is the last
951    /// place the two CAN be compared.
952    #[test]
953    fn boundary_factor_active_branch_agrees_with_the_direct_form_where_both_are_valid() {
954        const LAMBDA_REL_ACCURACY: f64 = 5.0e-14;
955        for &a in &[4.0_f64, 4.5, 6.0, 9.0, 20.0] {
956            for &b in &[0.0_f64, 0.25, 1.5] {
957                if b - a > LEFT_CONTINUED_FRACTION_SWITCH {
958                    continue; // not on the continued-fraction branch
959                }
960                let (_, d_a, _) = cone_boundary_log_factor_and_derivatives(a, b);
961                let (_, mills) = signed_probit_logcdf_and_mills_ratio(b - a);
962                let direct = a - mills;
963                assert!(
964                    (d_a - direct).abs() <= LAMBDA_REL_ACCURACY * mills,
965                    "(a={a}, b={b}): continued fraction {d_a} vs direct {direct} \
966                     (gap {:.3e}, budget {:.3e})",
967                    (d_a - direct).abs(),
968                    LAMBDA_REL_ACCURACY * mills
969                );
970            }
971        }
972    }
973
974    /// A gradient off the domain must not read as usable next to a NaN value.
975    #[test]
976    fn boundary_factor_derivatives_are_nan_off_the_domain() {
977        for &(a, b) in &[
978            (-1.0_f64, 0.0_f64),
979            (1.0, -1.0),
980            (f64::NAN, 1.0),
981            (f64::INFINITY, 0.0),
982        ] {
983            let (v, d_a, d_b) = cone_boundary_log_factor_and_derivatives(a, b);
984            assert!(
985                v.is_nan() && d_a.is_nan() && d_b.is_nan(),
986                "(a={a}, b={b}) is off-domain: got value {v}, ∂a {d_a}, ∂b {d_b}"
987            );
988        }
989    }
990
991    /// Closed-form partials against finite differences of the value
992    /// (test-only FD; the production gradient consumes the analytic form).
993    /// The domain is `a, b ≥ 0`, so a coordinate sitting exactly on the
994    /// boundary uses a one-sided forward difference instead of stepping
995    /// outside the domain (where the factor is deliberately NaN).
996    #[test]
997    fn boundary_factor_derivatives_match_finite_differences() {
998        let cases: [(f64, f64); 5] = [(0.3, 0.0), (2.0, 0.5), (0.0, 1.2), (5.0, 0.2), (0.7, 3.0)];
999        let step = 1e-6;
1000        let fd = |lo: f64, mid: f64, hi: f64, coord: f64| -> f64 {
1001            if coord >= step {
1002                (hi - lo) / (2.0 * step)
1003            } else {
1004                (hi - mid) / step
1005            }
1006        };
1007        for &(a, b) in &cases {
1008            let (_, d_a, d_b) = cone_boundary_log_factor_and_derivatives(a, b);
1009            let fd_a = fd(
1010                cone_boundary_log_factor((a - step).max(0.0), b),
1011                cone_boundary_log_factor(a, b),
1012                cone_boundary_log_factor(a + step, b),
1013                a,
1014            );
1015            let fd_b = fd(
1016                cone_boundary_log_factor(a, (b - step).max(0.0)),
1017                cone_boundary_log_factor(a, b),
1018                cone_boundary_log_factor(a, b + step),
1019                b,
1020            );
1021            // One-sided differences on boundary coordinates carry O(step)
1022            // truncation error, so the band is a few multiples of step.
1023            assert!(
1024                (d_a - fd_a).abs() <= 5e-6 * (1.0 + fd_a.abs()),
1025                "(a={a}, b={b}): ∂a analytic {d_a} vs FD {fd_a}"
1026            );
1027            assert!(
1028                (d_b - fd_b).abs() <= 5e-6 * (1.0 + fd_b.abs()),
1029                "(a={a}, b={b}): ∂b analytic {d_b} vs FD {fd_b}"
1030            );
1031        }
1032    }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038
1039    const TOL: f64 = 1e-12;
1040
1041    fn rel_err(got: f64, expected: f64) -> f64 {
1042        (got - expected).abs() / expected.abs().max(1e-300)
1043    }
1044
1045    #[test]
1046    fn beta_quantile_matches_known_reference_values() {
1047        let cases: [(f64, f64, f64, f64); 8] = [
1048            (0.025, 2.0, 2.0, 0.094_299_3),
1049            (0.975, 2.0, 2.0, 0.905_700_7),
1050            (0.5, 2.0, 2.0, 0.5),
1051            (0.025, 0.8, 4.0, 0.002_339_1),
1052            (0.975, 0.8, 4.0, 0.564_717_3),
1053            (0.025, 5.0, 1.5, 0.408_549_1),
1054            (0.5, 20.0, 80.0, 0.197_994_8),
1055            (0.975, 20.0, 80.0, 0.283_367_6),
1056        ];
1057        for (p, a, b, expected) in cases {
1058            let got = beta_quantile(p, a, b);
1059            let abs = (got - expected).abs();
1060            assert!(
1061                abs < 1e-5,
1062                "beta_quantile(p={p}, a={a}, b={b}) = {got}, expected ≈ {expected} (abs err {abs})"
1063            );
1064        }
1065    }
1066
1067    #[test]
1068    fn beta_quantile_boundaries_and_degeneracy() {
1069        assert_eq!(beta_quantile(0.0, 2.0, 3.0), 0.0);
1070        assert_eq!(beta_quantile(-0.5, 2.0, 3.0), 0.0);
1071        assert_eq!(beta_quantile(1.0, 2.0, 3.0), 1.0);
1072        assert_eq!(beta_quantile(1.5, 2.0, 3.0), 1.0);
1073        assert!(beta_quantile(0.5, -1.0, 3.0).is_nan());
1074        assert!(beta_quantile(0.5, 2.0, 0.0).is_nan());
1075        assert!(beta_quantile(0.5, f64::NAN, 3.0).is_nan());
1076        let mut prev = 0.0;
1077        for i in 1..100 {
1078            let p = i as f64 / 100.0;
1079            let q = beta_quantile(p, 3.0, 5.0);
1080            assert!(q > prev, "beta quantile not increasing at p={p}");
1081            prev = q;
1082        }
1083    }
1084
1085    // ── normal_pdf ────────────────────────────────────────────────────────────
1086
1087    #[test]
1088    fn normal_pdf_at_zero() {
1089        let expected = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
1090        assert!((normal_pdf(0.0) - expected).abs() < TOL);
1091    }
1092
1093    #[test]
1094    fn normal_pdf_symmetry() {
1095        for &x in &[0.5, 1.0, 2.0, 3.0, 5.0] {
1096            assert_eq!(normal_pdf(x), normal_pdf(-x), "symmetry failed at x={x}");
1097        }
1098    }
1099
1100    /// `x*x` is exact-splittable and the split is what `exp` needs.
1101    ///
1102    /// Two independent statements, because the correction is only worth what
1103    /// its residual is worth. First, `x*x + residual` is `x²` EXACTLY: checked
1104    /// against a Veltkamp/Dekker split, which reaches the same residual through
1105    /// pure multiplies and adds and shares no code path with the `mul_add`
1106    /// route. Second, the residual is not decorative — for these arguments it
1107    /// is a relative perturbation of `x²` big enough that `exp` amplifies it
1108    /// past a single ulp of the result.
1109    #[test]
1110    fn square_residual_completes_the_rounded_square_exactly() {
1111        // 2^27 + 1: Veltkamp's splitting factor, exact for any `x` whose
1112        // scaled form does not overflow.
1113        const SPLIT: f64 = 134_217_729.0;
1114        let mut saw_amplified = false;
1115        for &x in &[
1116            0.1, 0.7, 1.3, 2.9, 6.1, 10.5, 14.3, 19.7, 23.9, 25.9999, 34.7,
1117        ] {
1118            let rounded = x * x;
1119            let residual = square_residual(x, rounded);
1120
1121            let c = x * SPLIT;
1122            let head = c - (c - x);
1123            let tail = x - head;
1124            let dekker = ((head * head - rounded) + 2.0 * head * tail) + tail * tail;
1125            assert_eq!(
1126                residual, dekker,
1127                "x={x}: mul_add residual {residual:e} != Dekker residual {dekker:e}"
1128            );
1129
1130            // `exp` multiplies a relative argument perturbation by the argument.
1131            let amplified = (residual / rounded).abs() * rounded;
1132            if amplified > f64::EPSILON {
1133                saw_amplified = true;
1134            }
1135        }
1136        assert!(
1137            saw_amplified,
1138            "no test argument had a residual `exp` could amplify past one ulp; \
1139             the correction under test would be untested"
1140        );
1141    }
1142
1143    /// `φ(x)` against an EXTERNAL high-precision reference (mpmath, dps=60).
1144    ///
1145    /// Every argument here has an INEXACT square, which is the whole point.
1146    /// `exp(−½·fl(x*x))` misplaces the argument by `x²·ε/2` RELATIVE, and `exp`
1147    /// hands that straight back as relative error in the result: `1.4e-14` at
1148    /// `x ≈ 17`, `5.7e-14` by `x ≈ 35`, where `φ` is still a normal `f64`. Only
1149    /// the top of the range makes that visible, so the table has to reach it —
1150    /// a `φ` table that stops at `x = 5` cannot tell the two forms apart.
1151    ///
1152    /// `1.5e-15` (≈7 ulp) is the portability allowance: `f64::exp` is the
1153    /// platform libm and the only part of this that is not fixed by the crate
1154    /// graph, and it is worth ~1 ulp on the implementations in use. That still
1155    /// leaves 38x of margin against the defect at the top of the table.
1156    #[test]
1157    fn normal_pdf_matches_high_precision_reference() {
1158        const TOLERANCE: f64 = 1.5e-15;
1159        let refs: &[(f64, f64)] = &[
1160            (0.5, 0.35206532676429947),
1161            (1.0, 0.24197072451914334),
1162            (2.5, 0.017528300493568537),
1163            (4.0, 0.00013383022576488534),
1164            (7.3, 1.0693837871541648e-12),
1165            (11.9, 7.090702668428078e-32),
1166            (17.4, 7.201308152719057e-67),
1167            (23.6, 4.555989824112156e-122),
1168            (29.1, 5.229437243665329e-185),
1169            (34.7, 1.368008224488383e-262),
1170        ];
1171        for &(x, reference) in refs {
1172            // The small arguments anchor the ordinary range; the large ones are
1173            // where the defect lives, and every one of THOSE has to have a
1174            // square `f64` cannot hold or it exercises nothing.
1175            assert!(
1176                x <= 5.0 || square_residual(x, x * x) != 0.0,
1177                "x={x} squares exactly, so it cannot exercise the correction"
1178            );
1179            let rel = rel_err(normal_pdf(x), reference);
1180            assert!(
1181                rel < TOLERANCE,
1182                "normal_pdf({x}) = {:.17e}, reference {reference:.17e}, rel {rel:.3e}",
1183                normal_pdf(x)
1184            );
1185        }
1186    }
1187
1188    /// `φ` off the ordinary domain, where the square has no usable residual:
1189    /// `±∞` squares to `∞` and would hand the correction an `∞ − ∞`.
1190    #[test]
1191    fn normal_pdf_nonfinite_and_underflowed_arguments() {
1192        assert_eq!(normal_pdf(f64::INFINITY), 0.0);
1193        assert_eq!(normal_pdf(f64::NEG_INFINITY), 0.0);
1194        assert!(normal_pdf(f64::NAN).is_nan());
1195        // Past ~38.6 the pdf underflows; it must reach zero, not NaN.
1196        assert_eq!(normal_pdf(40.0), 0.0);
1197        assert_eq!(normal_pdf(-40.0), 0.0);
1198        assert_eq!(normal_pdf(f64::MAX), 0.0);
1199        // Just inside the underflow edge the result is subnormal but positive.
1200        let edge = normal_pdf(38.0);
1201        assert!(edge > 0.0 && edge.is_subnormal(), "phi(38) = {edge:e}");
1202    }
1203
1204    #[test]
1205    fn normal_pdf_positive() {
1206        for &x in &[-5.0, -1.0, 0.0, 1.0, 5.0] {
1207            assert!(normal_pdf(x) > 0.0, "pdf should be positive at x={x}");
1208        }
1209    }
1210
1211    // ── normal_cdf ────────────────────────────────────────────────────────────
1212
1213    #[test]
1214    fn normal_cdf_at_zero_is_half() {
1215        assert!((normal_cdf(0.0) - 0.5).abs() < TOL);
1216    }
1217
1218    #[test]
1219    fn normal_cdf_symmetry() {
1220        for &x in &[0.5, 1.0, 2.0, 3.0] {
1221            let sum = normal_cdf(x) + normal_cdf(-x);
1222            assert!(
1223                (sum - 1.0).abs() < TOL,
1224                "cdf symmetry failed at x={x}: sum={sum}"
1225            );
1226        }
1227    }
1228
1229    #[test]
1230    fn normal_cdf_bounds() {
1231        assert!(normal_cdf(10.0) > 0.9999);
1232        assert!(normal_cdf(-10.0) < 1e-22);
1233        assert!(normal_cdf(0.0) > 0.0);
1234        assert!(normal_cdf(0.0) < 1.0);
1235    }
1236
1237    #[test]
1238    fn normal_cdf_at_1_96_near_0975() {
1239        // Phi(1.96) ≈ 0.975 — canonical two-sided 5% critical value.
1240        let p = normal_cdf(1.959_963_985);
1241        assert!((p - 0.975).abs() < 1e-8, "p={p}");
1242    }
1243
1244    // ── erfcx_nonnegative ─────────────────────────────────────────────────────
1245
1246    #[test]
1247    fn erfcx_zero_is_one_and_negative_domain_is_rejected() {
1248        assert_eq!(erfcx_nonnegative(0.0), 1.0);
1249        assert!(erfcx_nonnegative(-f64::MIN_POSITIVE).is_nan());
1250        assert!(erfcx_nonnegative(-1.0).is_nan());
1251        assert!(erfcx_nonnegative(f64::NEG_INFINITY).is_nan());
1252    }
1253
1254    #[test]
1255    fn erfcx_positive_inf_returns_zero() {
1256        assert_eq!(erfcx_nonnegative(f64::INFINITY), 0.0);
1257    }
1258
1259    #[test]
1260    fn erfcx_nan_propagates() {
1261        assert!(erfcx_nonnegative(f64::NAN).is_nan());
1262    }
1263
1264    #[test]
1265    fn erfcx_small_positive_matches_direct() {
1266        use libm::erfc;
1267        for &x in &[0.1_f64, 0.5, 1.0, 5.0, 10.0, 25.0] {
1268            let got = erfcx_nonnegative(x);
1269            let expected = (x * x).exp() * erfc(x);
1270            let err = rel_err(got, expected);
1271            assert!(
1272                err < 1e-10,
1273                "x={x}: got={got} expected={expected} rel={err}"
1274            );
1275        }
1276    }
1277
1278    #[test]
1279    fn erfcx_large_x_positive_and_finite() {
1280        // For x >= 26 the asymptotic branch must remain positive and finite.
1281        let got = erfcx_nonnegative(50.0);
1282        assert!(got.is_finite() && got > 0.0, "erfcx(50)={got}");
1283        // Leading asymptotic term: 1/(x*sqrt(pi)).
1284        let asymptotic = 1.0 / (50.0 * std::f64::consts::PI.sqrt());
1285        assert!(
1286            rel_err(got, asymptotic) < 1e-3,
1287            "got={got} asymptotic={asymptotic}"
1288        );
1289    }
1290
1291    /// The two branches must describe one function across `x = 26`.
1292    ///
1293    /// Note WHY the plain `exp(x*x)·erfc(x)` below is a legitimate oracle at
1294    /// this particular argument and nowhere else: `26² = 676` is exactly
1295    /// representable, so the rounded square carries no residual and the direct
1296    /// form is momentarily as good as the corrected one. That is also exactly
1297    /// why this check was blind to the `x²·ε/2` defect it looks like it should
1298    /// have caught — at `25.9` the same comparison would have failed by
1299    /// `5.7e-14`, but the seam was only ever probed at the one point in the
1300    /// neighbourhood where the defect vanishes. The bit-adjacent step below
1301    /// cannot substitute for it either: `d(ln erfcx)/dx ≈ −2x` at the switch,
1302    /// so one ulp of `x` moves the true value by `1.8e-13`, three times the
1303    /// defect. It takes a reference at a DISTANCE from the seam — the table in
1304    /// `erfcx_matches_high_precision_reference` — to see the defect at all.
1305    #[test]
1306    fn erfcx_asymptotic_switch_matches_finite_direct_identity() {
1307        let switch = 26.0_f64;
1308        assert_eq!(
1309            square_residual(switch, switch * switch),
1310            0.0,
1311            "676 must be exact for the direct form below to be an oracle"
1312        );
1313        let direct = (switch * switch).exp() * erfc(switch);
1314        let asymptotic = erfcx_nonnegative(switch);
1315        assert!(
1316            rel_err(asymptotic, direct) < 1.0e-15,
1317            "switch mismatch: asymptotic={asymptotic:.17e}, direct={direct:.17e}"
1318        );
1319
1320        // Continuity across the branch cut, up to how fast the function itself
1321        // moves over one ulp of `x` (`|d ln erfcx/dx| ≈ 2x` ⇒ ~1.9e-13 here).
1322        let immediately_below = f64::from_bits(switch.to_bits() - 1);
1323        let below = erfcx_nonnegative(immediately_below);
1324        let step = 2.0 * switch * (switch - immediately_below);
1325        assert!(
1326            rel_err(asymptotic, below) < 2.0 * step,
1327            "discontinuous switch: below={below:.17e}, at={asymptotic:.17e}, \
1328             one-ulp travel {step:.3e}"
1329        );
1330    }
1331
1332    #[test]
1333    fn erfcx_preserves_representable_subnormal_tail() {
1334        let tail = erfcx_nonnegative(f64::MAX);
1335        assert!(tail > 0.0 && tail.is_subnormal(), "erfcx(MAX)={tail:e}");
1336    }
1337
1338    /// Absolute-accuracy pin against an EXTERNAL high-precision reference
1339    /// (mpmath, dps=60) spanning the direct branch `[0.1, 26)`. This is the
1340    /// root-cause guard: the previous `exp(x²)·erfc(x)` direct form was built on
1341    /// `statrs::erfc`, whose ~1e-10 relative accuracy silently poisoned every
1342    /// downstream probit / Mills / log-CDF derivative.
1343    ///
1344    /// The table had a SECOND job it was not doing. Of its twelve arguments,
1345    /// eleven — `0.5`, `2`, `3.5`, `6`, `9`, `13`, `18`, `22`, `25.5`, and the
1346    /// two whose squares are far too small to matter — square EXACTLY in `f64`,
1347    /// so `fl(x*x) = x²` and the `x²·ε/2` error the rounded square feeds `exp`
1348    /// was identically zero at every one of them. The twelfth, `25.9999`, does
1349    /// not square exactly; it was the one point in the table where the defect
1350    /// was live, and its literal had been recorded WITH the defect in it —
1351    /// `0.021683668126370212` against a true `0.021683668126369115`, off by
1352    /// `5.1e-14`. Three independent high-precision routes (`exp(x²)·erfc(x)`,
1353    /// the 12-term asymptotic series, and a 400-level Laplace continued
1354    /// fraction) and `scipy.special.erfcx` all agree on the corrected value.
1355    /// A `1e-13` tolerance then accepted a reference that was itself wrong by
1356    /// half the tolerance, which is how a 190x accuracy defect sat under a
1357    /// test named for high precision.
1358    ///
1359    /// So the table now RUNS ON arguments with inexact squares (`10.5`,
1360    /// `14.3`, `19.7`, `23.9` alongside the original grid) and the tolerance is
1361    /// `1.5e-15` — 38x below the defect at the top of the range, and still ~7
1362    /// ulp of headroom for the platform `f64::exp` (the only part of this path
1363    /// not pinned by the crate graph; `erfc` comes from the `libm` crate and is
1364    /// identical everywhere).
1365    #[test]
1366    fn erfcx_matches_high_precision_reference() {
1367        const TOLERANCE: f64 = 1.5e-15;
1368        // (x, mpmath exp(x²)·erfc(x) at dps=60, rounded to f64).
1369        let refs: &[(f64, f64)] = &[
1370            (0.1, 0.8964569799691267),
1371            (0.5, 0.6156903441929259),
1372            (1.0, 0.427583576155807),
1373            (2.0, 0.25539567631050575),
1374            (3.5, 0.1552936556088943),
1375            (6.0, 0.09277656780053835),
1376            (9.0, 0.06230772403777468),
1377            (10.5, 0.05349189974656412),
1378            (13.0, 0.043271921864609694),
1379            (14.3, 0.0393580473372741),
1380            (18.0, 0.03129571781590521),
1381            (19.7, 0.028602309402825203),
1382            (22.0, 0.025618570005879453),
1383            (23.9, 0.023585649371803793),
1384            (25.5, 0.022108108052519827),
1385            (25.9999, 0.021683668126369115),
1386        ];
1387        for &(x, reference) in refs {
1388            let got = erfcx_nonnegative(x);
1389            let rel = rel_err(got, reference);
1390            assert!(
1391                rel < TOLERANCE,
1392                "erfcx({x}) = {got:.17e}, reference {reference:.17e}, rel {rel:.3e}"
1393            );
1394        }
1395        // The point of the added arguments: at least four of them must have a
1396        // square `f64` cannot hold, or the table is back to testing nothing.
1397        let inexact = refs
1398            .iter()
1399            .filter(|&&(x, _)| square_residual(x, x * x) != 0.0)
1400            .count();
1401        assert!(
1402            inexact >= 4,
1403            "only {inexact} of {} reference arguments have an inexact square",
1404            refs.len()
1405        );
1406    }
1407
1408    // ── log1mexp_positive ─────────────────────────────────────────────────────
1409
1410    #[test]
1411    fn log1mexp_at_zero_is_neg_inf() {
1412        assert_eq!(log1mexp_positive(0.0), f64::NEG_INFINITY);
1413    }
1414
1415    #[test]
1416    fn log1mexp_recovers_log_one_minus_exp() {
1417        // Verify exp(log1mexp(a)) + exp(-a) ≈ 1 for several a > 0. This
1418        // roundtrip avoids computing `(1 - exp(-a)).ln()` directly, which
1419        // suffers catastrophic cancellation for large a (e.g. a=20 where
1420        // `1.0 - exp(-20)` loses 9 decimal digits from the subtraction).
1421        for &a in &[0.001_f64, 0.5, std::f64::consts::LN_2, 1.0, 5.0, 20.0] {
1422            let lm = log1mexp_positive(a);
1423            let roundtrip = lm.exp() + (-a).exp();
1424            assert!(
1425                (roundtrip - 1.0).abs() < 1e-14,
1426                "a={a}: exp(log1mexp(a)) + exp(-a) = {roundtrip}, expected 1.0"
1427            );
1428        }
1429    }
1430
1431    #[test]
1432    fn log1mexp_at_ln2_is_neg_ln2() {
1433        let ln2 = std::f64::consts::LN_2;
1434        let got = log1mexp_positive(ln2);
1435        assert!((got - (-ln2)).abs() < TOL, "got={got}");
1436    }
1437
1438    // ── signed_log_sum_exp ────────────────────────────────────────────────────
1439
1440    #[test]
1441    fn slse_all_positive_single() {
1442        let (lm, sg) = signed_log_sum_exp(&[2.0], &[1.0]);
1443        assert!((lm - 2.0).abs() < TOL);
1444        assert!((sg - 1.0).abs() < TOL);
1445    }
1446
1447    #[test]
1448    fn slse_difference_recovers_log2() {
1449        // 3 - 1 = 2 → log|2| = ln(2), sign = +1.
1450        let log3 = 3.0_f64.ln();
1451        let log1 = 0.0_f64; // ln(1)
1452        let (lm, sg) = signed_log_sum_exp(&[log3, log1], &[1.0, -1.0]);
1453        assert!((lm - 2.0_f64.ln()).abs() < TOL, "lm={lm}");
1454        assert!((sg - 1.0).abs() < TOL, "sg={sg}");
1455    }
1456
1457    #[test]
1458    fn slse_cancellation_gives_neg_inf() {
1459        // a - a = 0 → log|0| = -∞.
1460        let ln2 = 2.0_f64.ln();
1461        let (lm, sg) = signed_log_sum_exp(&[ln2, ln2], &[1.0, -1.0]);
1462        assert_eq!(lm, f64::NEG_INFINITY);
1463        assert_eq!(sg, 0.0);
1464    }
1465
1466    #[test]
1467    fn slse_empty_returns_neg_inf_with_zero_sign() {
1468        // With no terms the sum is exactly 0, so the docstring contract is
1469        // `(−∞, 0.0)`. (This test previously encoded the buggy `+1.0` positive-sum
1470        // convention, which contradicted both the docstring and the cancellation
1471        // test below; rewritten to the correct zero sign.)
1472        let (lm, sg) = signed_log_sum_exp(&[], &[]);
1473        assert_eq!(lm, f64::NEG_INFINITY);
1474        assert_eq!(sg, 0.0);
1475    }
1476
1477    #[test]
1478    fn slse_all_zero_signs_return_zero_sign() {
1479        // A single term whose sign is 0 contributes nothing; S = 0 ⇒ (−∞, 0.0).
1480        let (lm, sg) = signed_log_sum_exp(&[0.0], &[0.0]);
1481        assert_eq!(lm, f64::NEG_INFINITY);
1482        assert_eq!(sg, 0.0);
1483    }
1484
1485    #[test]
1486    fn slse_all_neg_inf_magnitudes_return_zero_sign() {
1487        // Every magnitude is exp(−∞) = 0 regardless of sign, so the sum is 0 and
1488        // the reported sign must be 0.0, not +1.0.
1489        let (lm, sg) = signed_log_sum_exp(&[f64::NEG_INFINITY, f64::NEG_INFINITY], &[1.0, -1.0]);
1490        assert_eq!(lm, f64::NEG_INFINITY);
1491        assert_eq!(sg, 0.0);
1492    }
1493
1494    #[test]
1495    fn slse_pos_inf_dominates() {
1496        let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[1.0, -1.0]);
1497        assert_eq!(lm, f64::INFINITY);
1498        assert_eq!(sg, 1.0);
1499    }
1500
1501    #[test]
1502    fn slse_neg_inf_dominates() {
1503        let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[-1.0, 1.0]);
1504        assert_eq!(lm, f64::INFINITY);
1505        assert_eq!(sg, -1.0);
1506    }
1507
1508    #[test]
1509    fn slse_both_inf_signs_gives_nan() {
1510        let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, f64::INFINITY], &[1.0, -1.0]);
1511        assert!(lm.is_nan());
1512        assert_eq!(sg, 0.0);
1513    }
1514
1515    // ── normal_logcdf ─────────────────────────────────────────────────────────
1516
1517    #[test]
1518    fn logcdf_at_zero_is_log_half() {
1519        let got = normal_logcdf(0.0);
1520        let expected = 0.5_f64.ln();
1521        assert!((got - expected).abs() < TOL, "got={got}");
1522    }
1523
1524    #[test]
1525    fn logcdf_pos_inf_is_zero() {
1526        assert_eq!(normal_logcdf(f64::INFINITY), 0.0);
1527    }
1528
1529    #[test]
1530    fn logcdf_neg_inf_is_neg_inf() {
1531        assert_eq!(normal_logcdf(f64::NEG_INFINITY), f64::NEG_INFINITY);
1532    }
1533
1534    #[test]
1535    fn logcdf_nan_is_nan() {
1536        assert!(normal_logcdf(f64::NAN).is_nan());
1537    }
1538
1539    #[test]
1540    fn logcdf_matches_log_cdf_for_moderate_x() {
1541        for &x in &[-2.0_f64, -1.0, 0.0, 1.0, 2.0, 3.0] {
1542            let got = normal_logcdf(x);
1543            let expected = normal_cdf(x).ln();
1544            assert!(
1545                (got - expected).abs() < 1e-10,
1546                "x={x}: got={got} expected={expected}"
1547            );
1548        }
1549    }
1550
1551    #[test]
1552    fn logcdf_deep_left_tail_stays_finite() {
1553        // For very negative x, normal_cdf(x) underflows to 0, but logcdf should
1554        // remain finite and large-negative.
1555        let got = normal_logcdf(-20.0);
1556        assert!(got.is_finite() && got < -100.0, "logcdf(-20)={got}");
1557    }
1558
1559    #[test]
1560    fn logcdf_positive_tail_does_not_round_through_unit_cdf() {
1561        let x = 10.0_f64;
1562        let got = normal_logcdf(x);
1563        let expected = (-0.5 * erfc(x / std::f64::consts::SQRT_2)).ln_1p();
1564        assert!(
1565            got < 0.0,
1566            "logcdf(10) must retain its negative tail: {got:e}"
1567        );
1568        assert_eq!(got.to_bits(), expected.to_bits());
1569    }
1570
1571    #[test]
1572    fn log_cdf_quantile_round_trips_both_unrepresentable_tails() {
1573        for x in [-1.0e6, -40.0, -10.0, -2.0, 0.0, 2.0, 10.0] {
1574            let log_p = normal_logcdf(x);
1575            let recovered = standard_normal_quantile_from_log_cdf(log_p)
1576                .expect("finite strict log-CDF has a quantile");
1577            assert!(
1578                (recovered - x).abs() <= 2.0e-12 * x.abs().max(1.0),
1579                "log-quantile round trip at x={x}: log_p={log_p}, recovered={recovered}"
1580            );
1581        }
1582    }
1583
1584    // ── normal_logsf ─────────────────────────────────────────────────────────
1585
1586    #[test]
1587    fn logsf_at_zero_is_log_half() {
1588        let got = normal_logsf(0.0);
1589        let expected = 0.5_f64.ln();
1590        assert!((got - expected).abs() < TOL, "got={got}");
1591    }
1592
1593    #[test]
1594    fn logsf_mirrors_logcdf() {
1595        // logsf(x) = logcdf(-x) by definition.
1596        for &x in &[-3.0_f64, -1.0, 0.0, 1.0, 3.0] {
1597            assert_eq!(normal_logsf(x), normal_logcdf(-x));
1598        }
1599    }
1600
1601    // ── signed_probit_logcdf_and_mills_ratio ──────────────────────────────────
1602
1603    #[test]
1604    fn probit_at_pos_inf() {
1605        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::INFINITY);
1606        assert_eq!(lc, 0.0);
1607        assert_eq!(mr, 0.0);
1608    }
1609
1610    #[test]
1611    fn probit_at_neg_inf() {
1612        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NEG_INFINITY);
1613        assert_eq!(lc, f64::NEG_INFINITY);
1614        assert_eq!(mr, f64::INFINITY);
1615    }
1616
1617    #[test]
1618    fn probit_nan_propagates() {
1619        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NAN);
1620        assert!(lc.is_nan() && mr.is_nan());
1621    }
1622
1623    #[test]
1624    fn probit_at_zero_logcdf_and_mills() {
1625        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(0.0);
1626        assert!((lc - 0.5_f64.ln()).abs() < TOL, "lc={lc}");
1627        // phi(0)/Phi(0) = 0.3989.../0.5 ≈ 0.7979.
1628        assert!((mr - 0.797_884_560_802_865).abs() < 1e-10, "mr={mr}");
1629    }
1630
1631    #[test]
1632    fn probit_positive_branch_matches_logcdf() {
1633        for &x in &[0.5_f64, 1.0, 2.0, 3.0] {
1634            let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
1635            let lc_ref = normal_logcdf(x);
1636            let mr_ref = normal_pdf(x) / normal_cdf(x);
1637            assert!(
1638                (lc - lc_ref).abs() < 1e-10,
1639                "x={x}: lc={lc} lc_ref={lc_ref}"
1640            );
1641            assert!(
1642                (mr - mr_ref).abs() < 1e-10,
1643                "x={x}: mr={mr} mr_ref={mr_ref}"
1644            );
1645        }
1646    }
1647
1648    #[test]
1649    fn probit_negative_branch_matches_logcdf() {
1650        for &x in &[-0.5_f64, -1.0, -2.0, -5.0] {
1651            let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
1652            let lc_ref = normal_logcdf(x);
1653            assert!(
1654                (lc - lc_ref).abs() < 1e-10,
1655                "x={x}: lc={lc} lc_ref={lc_ref}"
1656            );
1657            assert!(mr.is_finite() && mr > 0.0, "x={x}: mr={mr}");
1658        }
1659    }
1660
1661    #[test]
1662    fn probit_mills_ratio_has_no_deep_tail_floor() {
1663        let x = -1.0e305_f64;
1664        let (log_cdf, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
1665        assert_eq!(log_cdf, f64::NEG_INFINITY);
1666        assert!(mills_ratio.is_finite());
1667        assert!(
1668            ((mills_ratio / -x) - 1.0).abs() < 5.0e-15,
1669            "mills({x:e})={mills_ratio:e}"
1670        );
1671    }
1672
1673    #[test]
1674    fn normal_logcdf_derivative_stack_has_honest_infinite_limits() {
1675        assert_eq!(normal_logcdf_derivatives(f64::INFINITY), [0.0; 5]);
1676        assert_eq!(
1677            normal_logcdf_derivatives(f64::NEG_INFINITY),
1678            [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0]
1679        );
1680        assert!(
1681            normal_logcdf_derivatives(f64::NAN)
1682                .into_iter()
1683                .all(f64::is_nan)
1684        );
1685
1686        for x in [-1.0e200_f64, 1.0e200_f64] {
1687            let derivatives = normal_logcdf_derivatives(x);
1688            assert!(
1689                derivatives.into_iter().all(|value| !value.is_nan()),
1690                "NaN derivative at x={x:e}: {derivatives:?}"
1691            );
1692        }
1693    }
1694
1695    #[test]
1696    fn normal_logcdf_left_tail_derivatives_do_not_cancel() {
1697        let x = -1.0e100_f64;
1698        let derivatives = normal_logcdf_derivatives(x);
1699        assert_eq!(derivatives[2], -1.0);
1700        assert!(derivatives[3] > 0.0 && derivatives[3].is_finite());
1701        assert!(
1702            (derivatives[3] / 2.0e-300 - 1.0).abs() < 2.0e-14,
1703            "third derivative={:e}",
1704            derivatives[3]
1705        );
1706        assert_eq!(derivatives[4], 0.0);
1707    }
1708
1709    #[test]
1710    fn normal_logcdf_right_tail_preserves_weighted_subnormal_derivatives() {
1711        let derivatives = normal_logcdf_derivatives(38.6);
1712        assert_eq!(derivatives[1], 0.0);
1713        assert!(derivatives[2] < 0.0 && derivatives[2].is_subnormal());
1714        assert!(derivatives[3] > 0.0 && derivatives[3].is_subnormal());
1715        assert!(derivatives[4] < 0.0 && derivatives[4].is_subnormal());
1716    }
1717
1718    #[test]
1719    fn normal_logcdf_tail_stack_is_finite_difference_consistent() {
1720        let h = 1.0e-4_f64;
1721        for x in [-8.0_f64, -4.0, 8.0, 20.0] {
1722            let center = normal_logcdf_derivatives(x);
1723            let left = normal_logcdf_derivatives(x - h);
1724            let right = normal_logcdf_derivatives(x + h);
1725            for order in 1..=3 {
1726                let finite_difference = (right[order] - left[order]) / (2.0 * h);
1727                let expected = center[order + 1];
1728                let relative = (finite_difference - expected).abs() / expected.abs().max(1.0e-300);
1729                assert!(
1730                    relative < 2.0e-5,
1731                    "x={x}, order={order}: fd={finite_difference:e}, expected={expected:e}, rel={relative:e}"
1732                );
1733            }
1734        }
1735    }
1736
1737    /// Absolute-accuracy pin of the full `ln Φ(x)` derivative tower against an
1738    /// EXTERNAL high-precision reference (mpmath, dps=60), covering all three
1739    /// branches (continued-fraction left tail at x=−4, the moderate Mills
1740    /// recurrence for x∈(−4, 8), and both signs). Before the `erfc` root-cause
1741    /// fix the moderate branch's `λ = φ/Φ` inherited `statrs::erfc`'s ~1e-10
1742    /// error, so `f''` was wrong by ~1e-9 near the −4 seam; this pins every
1743    /// entry to `2e-11` relative, catching that regression head-on rather than
1744    /// through a seam-straddling finite difference.
1745    #[test]
1746    fn normal_logcdf_derivative_tower_matches_high_precision_reference() {
1747        // (x, [value, f', f'', f''', f''''] from mpmath at dps=60).
1748        let refs: &[(f64, [f64; 5])] = &[
1749            (
1750                -4.0,
1751                [
1752                    -10.360101486527291,
1753                    4.2256071444894711,
1754                    -0.95332716160257737,
1755                    0.017856339307658426,
1756                    0.0095065764315958691,
1757                ],
1758            ),
1759            // Two points well inside the continued-fraction branch, where the
1760            // truncation the depth controls is the ONLY error source: at -4 the
1761            // branch is at its least converged, and these confirm it stays put.
1762            (
1763                -10.0,
1764                [
1765                    -53.231285150512471,
1766                    10.098093233962512,
1767                    -0.99055462217434374,
1768                    0.0017864003921165069,
1769                    0.00049785382237944016,
1770                ],
1771            ),
1772            (
1773                -6.0,
1774                [
1775                    -20.736768949974706,
1776                    6.1584826045445989,
1777                    -0.97601236321083323,
1778                    0.0069535374991643118,
1779                    0.0028992056785575027,
1780                ],
1781            ),
1782            (
1783                -2.0,
1784                [
1785                    -3.7831843336820319,
1786                    2.3732155328228409,
1787                    -0.88572089958591874,
1788                    0.059355861291565813,
1789                    0.039421993865946813,
1790                ],
1791            ),
1792            (
1793                -1.0,
1794                [
1795                    -1.8410216450092635,
1796                    1.5251352761609812,
1797                    -0.80090233442965121,
1798                    0.11693119540604883,
1799                    0.07917498368074563,
1800                ],
1801            ),
1802            (
1803                -0.3,
1804                [
1805                    -0.96210281816885066,
1806                    0.99816596885848332,
1807                    -0.69688551072964971,
1808                    0.18398317992442132,
1809                    0.11037564722092704,
1810                ],
1811            ),
1812            (
1813                0.5,
1814                [
1815                    -0.36894641528865639,
1816                    0.50916043383703349,
1817                    -0.5138245643036329,
1818                    0.27099012446870783,
1819                    0.088167801929197554,
1820                ],
1821            ),
1822            (
1823                2.0,
1824                [
1825                    -0.023012909328963488,
1826                    0.055247862678989959,
1827                    -0.11354805168857645,
1828                    0.18439481503247759,
1829                    -0.18785468561160969,
1830                ],
1831            ),
1832        ];
1833        // The moderate-branch statrs regression produced ~1e-9 errors in f''.
1834        // The bound used to sit at 1e-10 to respect what was called the
1835        // continued-fraction branch's "inherent" ~2e-11 in f''''; that was not
1836        // inherent but a depth, and at 64 levels the branch reproduces this
1837        // 60-digit reference EXACTLY at x = -4, -6 and -10. What remains is the
1838        // moderate branch, where the brackets are already collected in `q` and
1839        // the floor is `λ`'s own relative error amplified by `λ/q` (18.7 at the
1840        // switch): 1.8e-13 at x = -2, the worst point here. 1e-11 keeps 55x of
1841        // headroom over that while still failing the 32-level truncation head-on.
1842        for &(x, reference) in refs {
1843            let got = normal_logcdf_derivatives(x);
1844            for (order, (&g, &r)) in got.iter().zip(reference.iter()).enumerate() {
1845                let rel = (g - r).abs() / r.abs().max(1.0e-3);
1846                assert!(
1847                    rel < 1.0e-11,
1848                    "normal_logcdf_derivatives({x})[{order}] = {g:.17e}, reference {r:.17e}, \
1849                     rel {rel:.3e} >= 1e-11"
1850                );
1851            }
1852        }
1853    }
1854
1855    // ── standard_normal_quantile ──────────────────────────────────────────────
1856
1857    #[test]
1858    fn quantile_rejects_out_of_range() {
1859        assert!(standard_normal_quantile(0.0).is_err());
1860        assert!(standard_normal_quantile(1.0).is_err());
1861        assert!(standard_normal_quantile(-0.1).is_err());
1862        assert!(standard_normal_quantile(1.1).is_err());
1863        assert!(standard_normal_quantile(f64::NAN).is_err());
1864    }
1865
1866    #[test]
1867    fn quantile_at_half_is_near_zero() {
1868        let q = standard_normal_quantile(0.5).unwrap();
1869        assert!(q.abs() < 1e-10, "quantile(0.5)={q}");
1870    }
1871
1872    #[test]
1873    fn quantile_at_0975_is_near_196() {
1874        let q = standard_normal_quantile(0.975).unwrap();
1875        assert!((q - 1.959_963_984_540_054).abs() < 1e-14, "q={q}");
1876    }
1877
1878    /// `standard_normal_quantile` and its log-CDF sibling, against a 120-digit
1879    /// root of `Φ(x) = p` (respectively `ln Φ(x) = log_p`).
1880    ///
1881    /// The seed is Acklam's rational approximation, whose accuracy is `1.15e-9`
1882    /// relative; the two Halley steps after it are what make the result
1883    /// ulp-accurate. Deleting the polish loop entirely leaves EVERY other
1884    /// quantile test in this module green except `quantile_roundtrip_cdf`, and
1885    /// that one only by a factor of 1.9 — so the polish had no real gate. This
1886    /// table is that gate: it fails by six orders if the seed ships unpolished.
1887    ///
1888    /// The grid straddles Acklam's own `P_LOW = 0.02425` branch on both sides,
1889    /// runs out to `p = 1e-300` where the seed is far from the root, and covers
1890    /// the reflected upper tail where the residual must be formed from
1891    /// `(1 − p) − ½erfc(x/√2)` rather than `Φ(x) − p`.
1892    #[test]
1893    fn normal_quantiles_match_independent_high_precision_reference() {
1894        const QUANTILE_REFERENCE: [[f64; 2]; 22] = [
1895            [1e-300, -37.0470962993612],
1896            [1e-100, -21.273453560965326],
1897            [1e-20, -9.262340089798407],
1898            [1e-08, -5.612001244174789],
1899            [0.001, -3.0902323061678136],
1900            [0.02424, -1.9731366119445441],
1901            [0.02425, -1.972961051311885],
1902            [0.02426, -1.9727855514678605],
1903            [0.05, -1.6448536269514726],
1904            [0.1, -1.2815515655446004],
1905            [0.25, -0.6744897501960817],
1906            [0.4, -0.2533471031357997],
1907            [0.5, 0.0],
1908            [0.6, 0.2533471031357997],
1909            [0.75, 0.6744897501960817],
1910            [0.9, 1.2815515655446006],
1911            [0.95, 1.6448536269514722],
1912            [0.975, 1.9599639845400538],
1913            [0.99, 2.3263478740408408],
1914            [0.999, 3.090232306167813],
1915            [0.99999999, 5.612001243305505],
1916            [0.9999999999999999, 8.209536151601387],
1917        ];
1918        for [p, want] in QUANTILE_REFERENCE {
1919            let got = standard_normal_quantile(p).expect("p in (0,1) has a quantile");
1920            let error = (got - want).abs();
1921            // `Φ⁻¹(½) = 0` exactly, so it is the one absolute comparison.
1922            let budget = if want == 0.0 {
1923                1e-16
1924            } else {
1925                4e-15 * want.abs()
1926            };
1927            assert!(
1928                error <= budget,
1929                "Φ⁻¹({p}): got {got:.17e}, want {want:.17e} (error {error:.3e} > {budget:.3e})"
1930            );
1931        }
1932
1933        const LOG_CDF_QUANTILE_REFERENCE: [[f64; 2]; 9] = [
1934            [-0.7, -0.008559478582480282],
1935            [-2.0, -1.1015196284987503],
1936            [-10.0, -3.913946240531893],
1937            [-50.0, -9.674825283612357],
1938            [-200.0, -19.803669380301212],
1939            [-1000.0, -44.6157477319694],
1940            [-10000.0, -141.37983987312717],
1941            [-100000.0, -447.1978936785251],
1942            [-1000000.0, -1414.2077829910174],
1943        ];
1944        for [log_p, want] in LOG_CDF_QUANTILE_REFERENCE {
1945            let got =
1946                standard_normal_quantile_from_log_cdf(log_p).expect("finite log_p < 0 has a root");
1947            let error = (got - want).abs();
1948            // Rounding `log_p` itself to `f64` already moves the root by
1949            // `ulp(log_p)·dx/d(log_p)`, and `dx/d(log_p) = Φ/φ = 1/λ` — about
1950            // `1.25` near `p = ½` and `≈ 1/|x|` in the deep tail. That input
1951            // conditioning, not the solver, is what limits `log_p = −0.7`,
1952            // where the root sits at `−0.00856` and one ulp of `0.7` is already
1953            // `1.4e-16` of it.
1954            let conditioning = 8.0 * f64::EPSILON * log_p.abs() / want.abs().max(0.8);
1955            let budget = 4e-15 * want.abs() + conditioning;
1956            assert!(
1957                error <= budget,
1958                "Φ⁻¹(exp({log_p})): got {got:.17e}, want {want:.17e} \
1959                 (error {error:.3e} > {budget:.3e})"
1960            );
1961        }
1962    }
1963
1964    #[test]
1965    fn quantile_antisymmetry() {
1966        let q_lo = standard_normal_quantile(0.1).unwrap();
1967        let q_hi = standard_normal_quantile(0.9).unwrap();
1968        assert!((q_lo + q_hi).abs() < 1e-10, "q_lo={q_lo} q_hi={q_hi}");
1969    }
1970
1971    #[test]
1972    fn quantile_roundtrip_cdf() {
1973        for &p in &[
1974            0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999,
1975        ] {
1976            let q = standard_normal_quantile(p).unwrap();
1977            let p_back = normal_cdf(q);
1978            // RELATIVE, and sized by what the round trip can cost: a few ulp of
1979            // `q` propagated through `φ(q)`, plus a couple of ulp from `erfc`
1980            // itself. The former absolute `1e-10` bar was two orders looser than
1981            // an unpolished Acklam seed at its worst point.
1982            assert!(
1983                (p_back - p).abs() <= 1e-14 * p,
1984                "roundtrip failed at p={p}: q={q} p_back={p_back}"
1985            );
1986        }
1987    }
1988}