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
7pub 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#[inline]
28pub fn normal_pdf(x: f64) -> f64 {
29 const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
30 INV_SQRT_2PI * (-0.5 * x * x).exp()
31}
32
33#[inline]
42pub fn normal_cdf(x: f64) -> f64 {
43 0.5 * erfc(-x / std::f64::consts::SQRT_2)
44}
45
46#[inline]
56pub fn erfcx_nonnegative(x: f64) -> f64 {
57 if x.is_nan() || x < 0.0 {
58 return f64::NAN;
59 }
60 if x == f64::INFINITY {
61 return 0.0;
62 }
63 if x < 26.0 {
64 (x * x).exp() * erfc(x)
65 } else {
66 let inv = 1.0 / x;
67 let inv2 = inv * inv;
68 let poly = 1.0
71 + inv2
72 * (-0.5
73 + inv2
74 * (0.75
75 + inv2
76 * (-1.875
77 + inv2 * (6.5625 + inv2 * (-29.53125 + inv2 * 162.421875)))));
78 inv * poly * INV_SQRT_PI
79 }
80}
81
82#[inline]
84pub fn log1mexp_positive(a: f64) -> f64 {
85 assert!(a >= 0.0, "log1mexp_positive requires a >= 0: a={a}");
86 if a > core::f64::consts::LN_2 {
87 (-(-a).exp()).ln_1p()
88 } else if a > 0.0 {
89 (-(-a).exp_m1()).ln()
90 } else {
91 f64::NEG_INFINITY
92 }
93}
94
95pub fn signed_log_sum_exp(log_mags: &[f64], signs: &[f64]) -> (f64, f64) {
113 let mut has_pos_inf = false;
117 let mut has_neg_inf = false;
118 for (idx, &lm) in log_mags.iter().enumerate() {
119 if lm == f64::INFINITY {
120 if signs[idx] > 0.0 {
121 has_pos_inf = true;
122 } else if signs[idx] < 0.0 {
123 has_neg_inf = true;
124 }
125 }
126 }
127 match (has_pos_inf, has_neg_inf) {
128 (true, true) => return (f64::NAN, 0.0),
130 (true, false) => return (f64::INFINITY, 1.0),
132 (false, true) => return (f64::INFINITY, -1.0),
134 (false, false) => {}
135 }
136
137 let mut pos_max = f64::NEG_INFINITY;
138 let mut neg_max = f64::NEG_INFINITY;
139 for (idx, &lm) in log_mags.iter().enumerate() {
140 if signs[idx] > 0.0 {
141 pos_max = pos_max.max(lm);
142 } else if signs[idx] < 0.0 {
143 neg_max = neg_max.max(lm);
144 }
145 }
146
147 let mut pos_sum = 0.0_f64;
148 let mut neg_sum = 0.0_f64;
149 for (idx, &lm) in log_mags.iter().enumerate() {
150 if !lm.is_finite() {
151 continue;
152 }
153 if signs[idx] > 0.0 {
154 pos_sum += (lm - pos_max).exp();
155 } else if signs[idx] < 0.0 {
156 neg_sum += (lm - neg_max).exp();
157 }
158 }
159
160 let log_pos = if pos_sum > 0.0 {
161 pos_max + pos_sum.ln()
162 } else {
163 f64::NEG_INFINITY
164 };
165 let log_neg = if neg_sum > 0.0 {
166 neg_max + neg_sum.ln()
167 } else {
168 f64::NEG_INFINITY
169 };
170
171 if log_pos == f64::NEG_INFINITY && log_neg == f64::NEG_INFINITY {
172 return (f64::NEG_INFINITY, 0.0);
178 }
179 if log_neg == f64::NEG_INFINITY {
180 return (log_pos, 1.0);
181 }
182 if log_pos == f64::NEG_INFINITY {
183 return (log_neg, -1.0);
184 }
185 if log_pos > log_neg {
186 let gap = log_pos - log_neg;
187 (log_pos + log1mexp_positive(gap), 1.0)
188 } else if log_neg > log_pos {
189 let gap = log_neg - log_pos;
190 (log_neg + log1mexp_positive(gap), -1.0)
191 } else {
192 (f64::NEG_INFINITY, 0.0)
193 }
194}
195
196#[inline]
204pub fn normal_logcdf(x: f64) -> f64 {
205 if x == f64::INFINITY {
206 return 0.0;
207 }
208 if x == f64::NEG_INFINITY {
209 return f64::NEG_INFINITY;
210 }
211 if x.is_nan() {
212 return f64::NAN;
213 }
214 if x < 0.0 {
215 let (u, scaled_tail) = negative_normal_tail_components(x);
216 negative_normal_logcdf_from_scaled_tail(u, scaled_tail)
217 } else {
218 let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
219 (-upper_tail).ln_1p()
220 }
221}
222
223#[inline]
227pub fn normal_logsf(x: f64) -> f64 {
228 normal_logcdf(-x)
229}
230
231#[inline]
238pub fn signed_probit_logcdf_and_mills_ratio(x: f64) -> (f64, f64) {
239 if x == f64::INFINITY {
240 return (0.0, 0.0);
241 }
242 if x == f64::NEG_INFINITY {
243 return (f64::NEG_INFINITY, f64::INFINITY);
244 }
245 if x.is_nan() {
246 return (f64::NAN, f64::NAN);
247 }
248 if x < 0.0 {
249 let (u, scaled_tail) = negative_normal_tail_components(x);
250 (
251 negative_normal_logcdf_from_scaled_tail(u, scaled_tail),
252 SQRT_2_OVER_PI / scaled_tail,
253 )
254 } else {
255 let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
256 let cdf = 1.0 - upper_tail;
257 let lambda = normal_pdf(x) / cdf;
258 ((-upper_tail).ln_1p(), lambda)
259 }
260}
261
262#[inline]
263fn negative_normal_tail_components(x: f64) -> (f64, f64) {
264 assert!(x.is_finite() && x < 0.0);
265 let u = -x / std::f64::consts::SQRT_2;
266 (u, erfcx_nonnegative(u))
267}
268
269#[inline]
270fn negative_normal_logcdf_from_scaled_tail(u: f64, scaled_tail: f64) -> f64 {
271 -u * u + scaled_tail.ln() - std::f64::consts::LN_2
272}
273
274#[inline]
286pub fn normal_logcdf_derivatives(x: f64) -> [f64; 5] {
287 if x.is_nan() {
288 return [f64::NAN; 5];
289 }
290 if x == f64::INFINITY {
291 return [0.0; 5];
292 }
293 if x == f64::NEG_INFINITY {
294 return [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0];
295 }
296
297 const LEFT_CONTINUED_FRACTION_SWITCH: f64 = -4.0;
298 const RIGHT_LOG_MAGNITUDE_SWITCH: f64 = 8.0;
299 if x <= LEFT_CONTINUED_FRACTION_SWITCH {
300 return normal_logcdf_derivatives_left_tail(x);
301 }
302 if x >= RIGHT_LOG_MAGNITUDE_SWITCH {
303 return normal_logcdf_derivatives_right_tail(x);
304 }
305
306 let (log_cdf, lambda) = signed_probit_logcdf_and_mills_ratio(x);
307 let lambda2 = lambda * lambda;
308 let lambda3 = lambda2 * lambda;
309 let x2 = x * x;
310 [
311 log_cdf,
312 lambda,
313 -lambda * (x + lambda),
314 lambda * (x2 - 1.0 + 3.0 * x * lambda + 2.0 * lambda2),
315 -lambda
316 * ((x * x2 - 3.0 * x) + (7.0 * x2 - 4.0) * lambda + 12.0 * x * lambda2 + 6.0 * lambda3),
317 ]
318}
319
320#[derive(Clone, Copy)]
321struct MillsCorrectionDerivatives {
322 value: f64,
323 first: f64,
324 second: f64,
325 third: f64,
326}
327
328#[inline]
329fn normal_logcdf_derivatives_left_tail(x: f64) -> [f64; 5] {
330 assert!(x.is_finite() && x <= -4.0);
331 let t = -x;
332 let mut q = MillsCorrectionDerivatives {
333 value: 0.0,
334 first: 0.0,
335 second: 0.0,
336 third: 0.0,
337 };
338 for n in (1..=32).rev() {
342 let denominator = t + q.value;
343 let inv_denominator = denominator.recip();
344 let value = f64::from(n) / denominator;
345 let denominator_first = 1.0 + q.first;
346 let a = denominator_first * inv_denominator;
347 let b = q.second * inv_denominator;
348 let c = q.third * inv_denominator;
349 q = MillsCorrectionDerivatives {
350 value,
351 first: -value * denominator_first / denominator,
352 second: value * (2.0 * a * a - b),
353 third: value * (-6.0 * a * a * a + 6.0 * a * b - c),
354 };
355 }
356 [
357 normal_logcdf(x),
358 t + q.value,
359 -(1.0 + q.first),
360 q.second,
361 -q.third,
362 ]
363}
364
365#[inline]
366fn normal_logcdf_derivatives_right_tail(x: f64) -> [f64; 5] {
367 assert!(x.is_finite() && x >= 8.0);
368 const LOG_SQRT_2PI: f64 = 0.918_938_533_204_672_7;
369 let log_cdf = normal_logcdf(x);
370 let u = x / std::f64::consts::SQRT_2;
371 let log_lambda = -u * u - LOG_SQRT_2PI - log_cdf;
372 let log_x = x.ln();
373 let inv_x2 = x.recip() * x.recip();
374
375 let first = log_lambda.exp();
376 let second = signed_exp_sum(&[log_x + log_lambda, 2.0 * log_lambda], &[-1.0, -1.0]);
377 let third = signed_exp_sum(
378 &[
379 2.0 * log_x + (-inv_x2).ln_1p() + log_lambda,
380 3.0_f64.ln() + log_x + 2.0 * log_lambda,
381 2.0_f64.ln() + 3.0 * log_lambda,
382 ],
383 &[1.0, 1.0, 1.0],
384 );
385 let fourth = signed_exp_sum(
386 &[
387 3.0 * log_x + (-3.0 * inv_x2).ln_1p() + log_lambda,
388 7.0_f64.ln() + 2.0 * log_x + (-(4.0 / 7.0) * inv_x2).ln_1p() + 2.0 * log_lambda,
389 12.0_f64.ln() + log_x + 3.0 * log_lambda,
390 6.0_f64.ln() + 4.0 * log_lambda,
391 ],
392 &[-1.0, -1.0, -1.0, -1.0],
393 );
394 [log_cdf, first, second, third, fourth]
395}
396
397#[inline]
398fn signed_exp_sum(log_magnitudes: &[f64], signs: &[f64]) -> f64 {
399 let (log_magnitude, sign) = signed_log_sum_exp(log_magnitudes, signs);
400 if sign == 0.0 {
401 0.0
402 } else {
403 sign * log_magnitude.exp()
404 }
405}
406
407#[inline]
409pub fn standard_normal_quantile(p: f64) -> Result<f64, String> {
410 if !(p.is_finite() && p > 0.0 && p < 1.0) {
411 return Err(format!("normal quantile requires p in (0,1), got {p}"));
412 }
413
414 const A: [f64; 6] = [
415 -3.969_683_028_665_376e1,
416 2.209_460_984_245_205e2,
417 -2.759_285_104_469_687e2,
418 1.383_577_518_672_69e2,
419 -3.066_479_806_614_716e1,
420 2.506_628_277_459_239,
421 ];
422 const B: [f64; 5] = [
423 -5.447_609_879_822_406e1,
424 1.615_858_368_580_409e2,
425 -1.556_989_798_598_866e2,
426 6.680_131_188_771_972e1,
427 -1.328_068_155_288_572e1,
428 ];
429 const C: [f64; 6] = [
430 -7.784_894_002_430_293e-3,
431 -3.223_964_580_411_365e-1,
432 -2.400_758_277_161_838,
433 -2.549_732_539_343_734,
434 4.374_664_141_464_968,
435 2.938_163_982_698_783,
436 ];
437 const D: [f64; 4] = [
438 7.784_695_709_041_462e-3,
439 3.224_671_290_700_398e-1,
440 2.445_134_137_142_996,
441 3.754_408_661_907_416,
442 ];
443 const P_LOW: f64 = 0.02425;
444 const P_HIGH: f64 = 1.0 - P_LOW;
445
446 let mut x = if p < P_LOW {
447 let q = (-2.0 * p.ln()).sqrt();
448 (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
449 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
450 } else if p <= P_HIGH {
451 let q = p - 0.5;
452 let r = q * q;
453 (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
454 / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
455 } else {
456 let q = (-2.0 * (1.0 - p).ln()).sqrt();
457 -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
458 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
459 };
460 for _ in 0..2 {
461 let density = normal_pdf(x);
462 if !(density.is_finite() && density > 0.0) {
463 break;
464 }
465 let residual = if x > 0.0 {
475 (1.0 - p) - 0.5 * erfc(x / std::f64::consts::SQRT_2)
476 } else {
477 normal_cdf(x) - p
478 };
479 let correction = residual / density;
480 let denominator = 1.0 + 0.5 * x * correction;
481 if !(correction.is_finite() && denominator.is_finite() && denominator != 0.0) {
482 break;
483 }
484 let step = correction / denominator;
485 if !step.is_finite() {
486 break;
487 }
488 x -= step;
489 if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
490 break;
491 }
492 }
493 Ok(x)
494}
495
496#[must_use]
528pub fn cone_boundary_log_factor(mu_over_sqrt_h: f64, slack_times_sqrt_h: f64) -> f64 {
529 let a = mu_over_sqrt_h;
530 let b = slack_times_sqrt_h;
531 if !(a.is_finite() && b.is_finite()) || a < 0.0 || b < 0.0 {
532 return f64::NAN;
533 }
534 let xi = b - a;
535 if xi >= 0.0 {
536 0.5 * a * a + normal_logcdf(xi)
540 } else {
541 let u = (a - b) / std::f64::consts::SQRT_2;
543 a * b - 0.5 * b * b + (0.5 * erfcx_nonnegative(u)).ln()
544 }
545}
546
547#[must_use]
559pub fn cone_boundary_log_factor_and_derivatives(
560 mu_over_sqrt_h: f64,
561 slack_times_sqrt_h: f64,
562) -> (f64, f64, f64) {
563 let value = cone_boundary_log_factor(mu_over_sqrt_h, slack_times_sqrt_h);
564 let xi = slack_times_sqrt_h - mu_over_sqrt_h;
565 let (_, mills) = signed_probit_logcdf_and_mills_ratio(xi);
566 (value, mu_over_sqrt_h - mills, mills)
567}
568
569#[cfg(test)]
570mod cone_boundary_factor_tests {
571 use super::*;
572
573 fn quadrature_log_relative_factor(mu: f64, h: f64, s: f64) -> f64 {
577 let upper = ((-mu / h) + 12.0 / h.sqrt()).max(-s + 12.0 / h.sqrt());
578 let lower = -s;
579 let n = 40_000usize;
580 let step = (upper - lower) / n as f64;
581 let f = |u: f64| (-mu * u - 0.5 * h * u * u).exp();
582 let mut acc = f(lower) + f(upper);
583 for i in 1..n {
584 let u = lower + step * i as f64;
585 acc += if i % 2 == 1 { 4.0 } else { 2.0 } * f(u);
586 }
587 let integral = acc * step / 3.0;
588 (integral / (2.0 * std::f64::consts::PI / h).sqrt()).ln()
589 }
590
591 #[test]
594 fn boundary_factor_matches_quadrature_across_regimes() {
595 let cases: [(f64, f64, f64); 8] = [
596 (0.0, 1.0, 0.0), (0.0, 4.0, 0.0), (2.5, 1.0, 0.0), (30.0, 9.0, 0.0), (0.0, 1.0, 0.7), (0.0, 2.0, 4.0), (1.5, 0.5, 2.0), (4.0, 2.0, 1.0), ];
605 for &(mu, h, s) in &cases {
606 let a = mu / h.sqrt();
607 let b = s * h.sqrt();
608 let closed = cone_boundary_log_factor(a, b);
609 let quad = quadrature_log_relative_factor(mu, h, s);
610 assert!(
611 (closed - quad).abs() <= 1e-9 * (1.0 + quad.abs()),
612 "(μ={mu}, h={h}, s={s}): closed {closed} vs quadrature {quad}"
613 );
614 }
615 assert!(
616 (cone_boundary_log_factor(0.0, 0.0) - 0.5_f64.ln()).abs() < 1e-15,
617 "an activating row must contribute exactly the half-Gaussian ln ½"
618 );
619 }
620
621 #[test]
626 fn boundary_factor_limits_are_exact() {
627 let a = 1.0e6;
628 let expected = -(a * (2.0 * std::f64::consts::PI).sqrt()).ln();
629 let got = cone_boundary_log_factor(a, 0.0);
630 assert!(
631 (got - expected).abs() <= 1e-9 * expected.abs(),
632 "deep-active: got {got}, expected {expected}"
633 );
634 let interior = cone_boundary_log_factor(0.0, 40.0);
635 assert!(
636 interior.abs() < 1e-300 || interior > -1e-12,
637 "deep-interior must vanish; got {interior}"
638 );
639 }
640
641 #[test]
647 fn boundary_factor_derivatives_match_finite_differences() {
648 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)];
649 let step = 1e-6;
650 let fd = |lo: f64, mid: f64, hi: f64, coord: f64| -> f64 {
651 if coord >= step {
652 (hi - lo) / (2.0 * step)
653 } else {
654 (hi - mid) / step
655 }
656 };
657 for &(a, b) in &cases {
658 let (_, d_a, d_b) = cone_boundary_log_factor_and_derivatives(a, b);
659 let fd_a = fd(
660 cone_boundary_log_factor((a - step).max(0.0), b),
661 cone_boundary_log_factor(a, b),
662 cone_boundary_log_factor(a + step, b),
663 a,
664 );
665 let fd_b = fd(
666 cone_boundary_log_factor(a, (b - step).max(0.0)),
667 cone_boundary_log_factor(a, b),
668 cone_boundary_log_factor(a, b + step),
669 b,
670 );
671 assert!(
674 (d_a - fd_a).abs() <= 5e-6 * (1.0 + fd_a.abs()),
675 "(a={a}, b={b}): ∂a analytic {d_a} vs FD {fd_a}"
676 );
677 assert!(
678 (d_b - fd_b).abs() <= 5e-6 * (1.0 + fd_b.abs()),
679 "(a={a}, b={b}): ∂b analytic {d_b} vs FD {fd_b}"
680 );
681 }
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 const TOL: f64 = 1e-12;
690
691 fn rel_err(got: f64, expected: f64) -> f64 {
692 (got - expected).abs() / expected.abs().max(1e-300)
693 }
694
695 #[test]
696 fn beta_quantile_matches_known_reference_values() {
697 let cases: [(f64, f64, f64, f64); 8] = [
698 (0.025, 2.0, 2.0, 0.094_299_3),
699 (0.975, 2.0, 2.0, 0.905_700_7),
700 (0.5, 2.0, 2.0, 0.5),
701 (0.025, 0.8, 4.0, 0.002_339_1),
702 (0.975, 0.8, 4.0, 0.564_717_3),
703 (0.025, 5.0, 1.5, 0.408_549_1),
704 (0.5, 20.0, 80.0, 0.197_994_8),
705 (0.975, 20.0, 80.0, 0.283_367_6),
706 ];
707 for (p, a, b, expected) in cases {
708 let got = beta_quantile(p, a, b);
709 let abs = (got - expected).abs();
710 assert!(
711 abs < 1e-5,
712 "beta_quantile(p={p}, a={a}, b={b}) = {got}, expected ≈ {expected} (abs err {abs})"
713 );
714 }
715 }
716
717 #[test]
718 fn beta_quantile_boundaries_and_degeneracy() {
719 assert_eq!(beta_quantile(0.0, 2.0, 3.0), 0.0);
720 assert_eq!(beta_quantile(-0.5, 2.0, 3.0), 0.0);
721 assert_eq!(beta_quantile(1.0, 2.0, 3.0), 1.0);
722 assert_eq!(beta_quantile(1.5, 2.0, 3.0), 1.0);
723 assert!(beta_quantile(0.5, -1.0, 3.0).is_nan());
724 assert!(beta_quantile(0.5, 2.0, 0.0).is_nan());
725 assert!(beta_quantile(0.5, f64::NAN, 3.0).is_nan());
726 let mut prev = 0.0;
727 for i in 1..100 {
728 let p = i as f64 / 100.0;
729 let q = beta_quantile(p, 3.0, 5.0);
730 assert!(q > prev, "beta quantile not increasing at p={p}");
731 prev = q;
732 }
733 }
734
735 #[test]
738 fn normal_pdf_at_zero() {
739 let expected = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
740 assert!((normal_pdf(0.0) - expected).abs() < TOL);
741 }
742
743 #[test]
744 fn normal_pdf_symmetry() {
745 for &x in &[0.5, 1.0, 2.0, 3.0, 5.0] {
746 assert_eq!(normal_pdf(x), normal_pdf(-x), "symmetry failed at x={x}");
747 }
748 }
749
750 #[test]
751 fn normal_pdf_positive() {
752 for &x in &[-5.0, -1.0, 0.0, 1.0, 5.0] {
753 assert!(normal_pdf(x) > 0.0, "pdf should be positive at x={x}");
754 }
755 }
756
757 #[test]
760 fn normal_cdf_at_zero_is_half() {
761 assert!((normal_cdf(0.0) - 0.5).abs() < TOL);
762 }
763
764 #[test]
765 fn normal_cdf_symmetry() {
766 for &x in &[0.5, 1.0, 2.0, 3.0] {
767 let sum = normal_cdf(x) + normal_cdf(-x);
768 assert!(
769 (sum - 1.0).abs() < TOL,
770 "cdf symmetry failed at x={x}: sum={sum}"
771 );
772 }
773 }
774
775 #[test]
776 fn normal_cdf_bounds() {
777 assert!(normal_cdf(10.0) > 0.9999);
778 assert!(normal_cdf(-10.0) < 1e-22);
779 assert!(normal_cdf(0.0) > 0.0);
780 assert!(normal_cdf(0.0) < 1.0);
781 }
782
783 #[test]
784 fn normal_cdf_at_1_96_near_0975() {
785 let p = normal_cdf(1.959_963_985);
787 assert!((p - 0.975).abs() < 1e-8, "p={p}");
788 }
789
790 #[test]
793 fn erfcx_zero_is_one_and_negative_domain_is_rejected() {
794 assert_eq!(erfcx_nonnegative(0.0), 1.0);
795 assert!(erfcx_nonnegative(-f64::MIN_POSITIVE).is_nan());
796 assert!(erfcx_nonnegative(-1.0).is_nan());
797 assert!(erfcx_nonnegative(f64::NEG_INFINITY).is_nan());
798 }
799
800 #[test]
801 fn erfcx_positive_inf_returns_zero() {
802 assert_eq!(erfcx_nonnegative(f64::INFINITY), 0.0);
803 }
804
805 #[test]
806 fn erfcx_nan_propagates() {
807 assert!(erfcx_nonnegative(f64::NAN).is_nan());
808 }
809
810 #[test]
811 fn erfcx_small_positive_matches_direct() {
812 use libm::erfc;
813 for &x in &[0.1_f64, 0.5, 1.0, 5.0, 10.0, 25.0] {
814 let got = erfcx_nonnegative(x);
815 let expected = (x * x).exp() * erfc(x);
816 let err = rel_err(got, expected);
817 assert!(
818 err < 1e-10,
819 "x={x}: got={got} expected={expected} rel={err}"
820 );
821 }
822 }
823
824 #[test]
825 fn erfcx_large_x_positive_and_finite() {
826 let got = erfcx_nonnegative(50.0);
828 assert!(got.is_finite() && got > 0.0, "erfcx(50)={got}");
829 let asymptotic = 1.0 / (50.0 * std::f64::consts::PI.sqrt());
831 assert!(
832 rel_err(got, asymptotic) < 1e-3,
833 "got={got} asymptotic={asymptotic}"
834 );
835 }
836
837 #[test]
838 fn erfcx_asymptotic_switch_matches_finite_direct_identity() {
839 let switch = 26.0_f64;
840 let direct = (switch * switch).exp() * erfc(switch);
841 let asymptotic = erfcx_nonnegative(switch);
842 assert!(
843 rel_err(asymptotic, direct) < 5.0e-14,
844 "switch mismatch: asymptotic={asymptotic:.17e}, direct={direct:.17e}"
845 );
846
847 let immediately_below = f64::from_bits(switch.to_bits() - 1);
848 let below = erfcx_nonnegative(immediately_below);
849 assert!(
850 rel_err(asymptotic, below) < 5.0e-14,
851 "discontinuous switch: below={below:.17e}, at={asymptotic:.17e}"
852 );
853 }
854
855 #[test]
856 fn erfcx_preserves_representable_subnormal_tail() {
857 let tail = erfcx_nonnegative(f64::MAX);
858 assert!(tail > 0.0 && tail.is_subnormal(), "erfcx(MAX)={tail:e}");
859 }
860
861 #[test]
870 fn erfcx_matches_high_precision_reference() {
871 let refs: &[(f64, f64)] = &[
873 (0.1, 0.89645697996912664),
874 (0.5, 0.61569034419292587),
875 (1.0, 0.427583576155807),
876 (2.0, 0.25539567631050574),
877 (3.5, 0.1552936556088943),
878 (6.0, 0.092776567800538354),
879 (9.0, 0.062307724037774684),
880 (13.0, 0.043271921864609693),
881 (18.0, 0.03129571781590521),
882 (22.0, 0.025618570005879453),
883 (25.5, 0.022108108052519827),
884 (25.9999, 0.021683668126370212),
885 ];
886 for &(x, reference) in refs {
887 let got = erfcx_nonnegative(x);
888 let rel = (got - reference).abs() / reference.abs();
889 assert!(
890 rel < 1.0e-13,
891 "erfcx({x}) = {got:.17e}, reference {reference:.17e}, rel {rel:.3e} >= 1e-13"
892 );
893 }
894 }
895
896 #[test]
899 fn log1mexp_at_zero_is_neg_inf() {
900 assert_eq!(log1mexp_positive(0.0), f64::NEG_INFINITY);
901 }
902
903 #[test]
904 fn log1mexp_recovers_log_one_minus_exp() {
905 for &a in &[0.001_f64, 0.5, std::f64::consts::LN_2, 1.0, 5.0, 20.0] {
910 let lm = log1mexp_positive(a);
911 let roundtrip = lm.exp() + (-a).exp();
912 assert!(
913 (roundtrip - 1.0).abs() < 1e-14,
914 "a={a}: exp(log1mexp(a)) + exp(-a) = {roundtrip}, expected 1.0"
915 );
916 }
917 }
918
919 #[test]
920 fn log1mexp_at_ln2_is_neg_ln2() {
921 let ln2 = std::f64::consts::LN_2;
922 let got = log1mexp_positive(ln2);
923 assert!((got - (-ln2)).abs() < TOL, "got={got}");
924 }
925
926 #[test]
929 fn slse_all_positive_single() {
930 let (lm, sg) = signed_log_sum_exp(&[2.0], &[1.0]);
931 assert!((lm - 2.0).abs() < TOL);
932 assert!((sg - 1.0).abs() < TOL);
933 }
934
935 #[test]
936 fn slse_difference_recovers_log2() {
937 let log3 = 3.0_f64.ln();
939 let log1 = 0.0_f64; let (lm, sg) = signed_log_sum_exp(&[log3, log1], &[1.0, -1.0]);
941 assert!((lm - 2.0_f64.ln()).abs() < TOL, "lm={lm}");
942 assert!((sg - 1.0).abs() < TOL, "sg={sg}");
943 }
944
945 #[test]
946 fn slse_cancellation_gives_neg_inf() {
947 let ln2 = 2.0_f64.ln();
949 let (lm, sg) = signed_log_sum_exp(&[ln2, ln2], &[1.0, -1.0]);
950 assert_eq!(lm, f64::NEG_INFINITY);
951 assert_eq!(sg, 0.0);
952 }
953
954 #[test]
955 fn slse_empty_returns_neg_inf_with_zero_sign() {
956 let (lm, sg) = signed_log_sum_exp(&[], &[]);
961 assert_eq!(lm, f64::NEG_INFINITY);
962 assert_eq!(sg, 0.0);
963 }
964
965 #[test]
966 fn slse_all_zero_signs_return_zero_sign() {
967 let (lm, sg) = signed_log_sum_exp(&[0.0], &[0.0]);
969 assert_eq!(lm, f64::NEG_INFINITY);
970 assert_eq!(sg, 0.0);
971 }
972
973 #[test]
974 fn slse_all_neg_inf_magnitudes_return_zero_sign() {
975 let (lm, sg) = signed_log_sum_exp(&[f64::NEG_INFINITY, f64::NEG_INFINITY], &[1.0, -1.0]);
978 assert_eq!(lm, f64::NEG_INFINITY);
979 assert_eq!(sg, 0.0);
980 }
981
982 #[test]
983 fn slse_pos_inf_dominates() {
984 let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[1.0, -1.0]);
985 assert_eq!(lm, f64::INFINITY);
986 assert_eq!(sg, 1.0);
987 }
988
989 #[test]
990 fn slse_neg_inf_dominates() {
991 let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[-1.0, 1.0]);
992 assert_eq!(lm, f64::INFINITY);
993 assert_eq!(sg, -1.0);
994 }
995
996 #[test]
997 fn slse_both_inf_signs_gives_nan() {
998 let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, f64::INFINITY], &[1.0, -1.0]);
999 assert!(lm.is_nan());
1000 assert_eq!(sg, 0.0);
1001 }
1002
1003 #[test]
1006 fn logcdf_at_zero_is_log_half() {
1007 let got = normal_logcdf(0.0);
1008 let expected = 0.5_f64.ln();
1009 assert!((got - expected).abs() < TOL, "got={got}");
1010 }
1011
1012 #[test]
1013 fn logcdf_pos_inf_is_zero() {
1014 assert_eq!(normal_logcdf(f64::INFINITY), 0.0);
1015 }
1016
1017 #[test]
1018 fn logcdf_neg_inf_is_neg_inf() {
1019 assert_eq!(normal_logcdf(f64::NEG_INFINITY), f64::NEG_INFINITY);
1020 }
1021
1022 #[test]
1023 fn logcdf_nan_is_nan() {
1024 assert!(normal_logcdf(f64::NAN).is_nan());
1025 }
1026
1027 #[test]
1028 fn logcdf_matches_log_cdf_for_moderate_x() {
1029 for &x in &[-2.0_f64, -1.0, 0.0, 1.0, 2.0, 3.0] {
1030 let got = normal_logcdf(x);
1031 let expected = normal_cdf(x).ln();
1032 assert!(
1033 (got - expected).abs() < 1e-10,
1034 "x={x}: got={got} expected={expected}"
1035 );
1036 }
1037 }
1038
1039 #[test]
1040 fn logcdf_deep_left_tail_stays_finite() {
1041 let got = normal_logcdf(-20.0);
1044 assert!(got.is_finite() && got < -100.0, "logcdf(-20)={got}");
1045 }
1046
1047 #[test]
1048 fn logcdf_positive_tail_does_not_round_through_unit_cdf() {
1049 let x = 10.0_f64;
1050 let got = normal_logcdf(x);
1051 let expected = (-0.5 * erfc(x / std::f64::consts::SQRT_2)).ln_1p();
1052 assert!(
1053 got < 0.0,
1054 "logcdf(10) must retain its negative tail: {got:e}"
1055 );
1056 assert_eq!(got.to_bits(), expected.to_bits());
1057 }
1058
1059 #[test]
1062 fn logsf_at_zero_is_log_half() {
1063 let got = normal_logsf(0.0);
1064 let expected = 0.5_f64.ln();
1065 assert!((got - expected).abs() < TOL, "got={got}");
1066 }
1067
1068 #[test]
1069 fn logsf_mirrors_logcdf() {
1070 for &x in &[-3.0_f64, -1.0, 0.0, 1.0, 3.0] {
1072 assert_eq!(normal_logsf(x), normal_logcdf(-x));
1073 }
1074 }
1075
1076 #[test]
1079 fn probit_at_pos_inf() {
1080 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::INFINITY);
1081 assert_eq!(lc, 0.0);
1082 assert_eq!(mr, 0.0);
1083 }
1084
1085 #[test]
1086 fn probit_at_neg_inf() {
1087 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NEG_INFINITY);
1088 assert_eq!(lc, f64::NEG_INFINITY);
1089 assert_eq!(mr, f64::INFINITY);
1090 }
1091
1092 #[test]
1093 fn probit_nan_propagates() {
1094 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NAN);
1095 assert!(lc.is_nan() && mr.is_nan());
1096 }
1097
1098 #[test]
1099 fn probit_at_zero_logcdf_and_mills() {
1100 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(0.0);
1101 assert!((lc - 0.5_f64.ln()).abs() < TOL, "lc={lc}");
1102 assert!((mr - 0.797_884_560_802_865).abs() < 1e-10, "mr={mr}");
1104 }
1105
1106 #[test]
1107 fn probit_positive_branch_matches_logcdf() {
1108 for &x in &[0.5_f64, 1.0, 2.0, 3.0] {
1109 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
1110 let lc_ref = normal_logcdf(x);
1111 let mr_ref = normal_pdf(x) / normal_cdf(x);
1112 assert!(
1113 (lc - lc_ref).abs() < 1e-10,
1114 "x={x}: lc={lc} lc_ref={lc_ref}"
1115 );
1116 assert!(
1117 (mr - mr_ref).abs() < 1e-10,
1118 "x={x}: mr={mr} mr_ref={mr_ref}"
1119 );
1120 }
1121 }
1122
1123 #[test]
1124 fn probit_negative_branch_matches_logcdf() {
1125 for &x in &[-0.5_f64, -1.0, -2.0, -5.0] {
1126 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
1127 let lc_ref = normal_logcdf(x);
1128 assert!(
1129 (lc - lc_ref).abs() < 1e-10,
1130 "x={x}: lc={lc} lc_ref={lc_ref}"
1131 );
1132 assert!(mr.is_finite() && mr > 0.0, "x={x}: mr={mr}");
1133 }
1134 }
1135
1136 #[test]
1137 fn probit_mills_ratio_has_no_deep_tail_floor() {
1138 let x = -1.0e305_f64;
1139 let (log_cdf, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
1140 assert_eq!(log_cdf, f64::NEG_INFINITY);
1141 assert!(mills_ratio.is_finite());
1142 assert!(
1143 ((mills_ratio / -x) - 1.0).abs() < 5.0e-15,
1144 "mills({x:e})={mills_ratio:e}"
1145 );
1146 }
1147
1148 #[test]
1149 fn normal_logcdf_derivative_stack_has_honest_infinite_limits() {
1150 assert_eq!(normal_logcdf_derivatives(f64::INFINITY), [0.0; 5]);
1151 assert_eq!(
1152 normal_logcdf_derivatives(f64::NEG_INFINITY),
1153 [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0]
1154 );
1155 assert!(
1156 normal_logcdf_derivatives(f64::NAN)
1157 .into_iter()
1158 .all(f64::is_nan)
1159 );
1160
1161 for x in [-1.0e200_f64, 1.0e200_f64] {
1162 let derivatives = normal_logcdf_derivatives(x);
1163 assert!(
1164 derivatives.into_iter().all(|value| !value.is_nan()),
1165 "NaN derivative at x={x:e}: {derivatives:?}"
1166 );
1167 }
1168 }
1169
1170 #[test]
1171 fn normal_logcdf_left_tail_derivatives_do_not_cancel() {
1172 let x = -1.0e100_f64;
1173 let derivatives = normal_logcdf_derivatives(x);
1174 assert_eq!(derivatives[2], -1.0);
1175 assert!(derivatives[3] > 0.0 && derivatives[3].is_finite());
1176 assert!(
1177 (derivatives[3] / 2.0e-300 - 1.0).abs() < 2.0e-14,
1178 "third derivative={:e}",
1179 derivatives[3]
1180 );
1181 assert_eq!(derivatives[4], 0.0);
1182 }
1183
1184 #[test]
1185 fn normal_logcdf_right_tail_preserves_weighted_subnormal_derivatives() {
1186 let derivatives = normal_logcdf_derivatives(38.6);
1187 assert_eq!(derivatives[1], 0.0);
1188 assert!(derivatives[2] < 0.0 && derivatives[2].is_subnormal());
1189 assert!(derivatives[3] > 0.0 && derivatives[3].is_subnormal());
1190 assert!(derivatives[4] < 0.0 && derivatives[4].is_subnormal());
1191 }
1192
1193 #[test]
1194 fn normal_logcdf_tail_stack_is_finite_difference_consistent() {
1195 let h = 1.0e-4_f64;
1196 for x in [-8.0_f64, -4.0, 8.0, 20.0] {
1197 let center = normal_logcdf_derivatives(x);
1198 let left = normal_logcdf_derivatives(x - h);
1199 let right = normal_logcdf_derivatives(x + h);
1200 for order in 1..=3 {
1201 let finite_difference = (right[order] - left[order]) / (2.0 * h);
1202 let expected = center[order + 1];
1203 let relative = (finite_difference - expected).abs() / expected.abs().max(1.0e-300);
1204 assert!(
1205 relative < 2.0e-5,
1206 "x={x}, order={order}: fd={finite_difference:e}, expected={expected:e}, rel={relative:e}"
1207 );
1208 }
1209 }
1210 }
1211
1212 #[test]
1221 fn normal_logcdf_derivative_tower_matches_high_precision_reference() {
1222 let refs: &[(f64, [f64; 5])] = &[
1224 (
1225 -4.0,
1226 [
1227 -10.360101486527291,
1228 4.2256071444894711,
1229 -0.95332716160257737,
1230 0.017856339307658426,
1231 0.0095065764315958691,
1232 ],
1233 ),
1234 (
1235 -2.0,
1236 [
1237 -3.7831843336820319,
1238 2.3732155328228409,
1239 -0.88572089958591874,
1240 0.059355861291565813,
1241 0.039421993865946813,
1242 ],
1243 ),
1244 (
1245 -1.0,
1246 [
1247 -1.8410216450092635,
1248 1.5251352761609812,
1249 -0.80090233442965121,
1250 0.11693119540604883,
1251 0.07917498368074563,
1252 ],
1253 ),
1254 (
1255 -0.3,
1256 [
1257 -0.96210281816885066,
1258 0.99816596885848332,
1259 -0.69688551072964971,
1260 0.18398317992442132,
1261 0.11037564722092704,
1262 ],
1263 ),
1264 (
1265 0.5,
1266 [
1267 -0.36894641528865639,
1268 0.50916043383703349,
1269 -0.5138245643036329,
1270 0.27099012446870783,
1271 0.088167801929197554,
1272 ],
1273 ),
1274 (
1275 2.0,
1276 [
1277 -0.023012909328963488,
1278 0.055247862678989959,
1279 -0.11354805168857645,
1280 0.18439481503247759,
1281 -0.18785468561160969,
1282 ],
1283 ),
1284 ];
1285 for &(x, reference) in refs {
1290 let got = normal_logcdf_derivatives(x);
1291 for (order, (&g, &r)) in got.iter().zip(reference.iter()).enumerate() {
1292 let rel = (g - r).abs() / r.abs().max(1.0e-3);
1293 assert!(
1294 rel < 1.0e-10,
1295 "normal_logcdf_derivatives({x})[{order}] = {g:.17e}, reference {r:.17e}, \
1296 rel {rel:.3e} >= 1e-10"
1297 );
1298 }
1299 }
1300 }
1301
1302 #[test]
1305 fn quantile_rejects_out_of_range() {
1306 assert!(standard_normal_quantile(0.0).is_err());
1307 assert!(standard_normal_quantile(1.0).is_err());
1308 assert!(standard_normal_quantile(-0.1).is_err());
1309 assert!(standard_normal_quantile(1.1).is_err());
1310 assert!(standard_normal_quantile(f64::NAN).is_err());
1311 }
1312
1313 #[test]
1314 fn quantile_at_half_is_near_zero() {
1315 let q = standard_normal_quantile(0.5).unwrap();
1316 assert!(q.abs() < 1e-10, "quantile(0.5)={q}");
1317 }
1318
1319 #[test]
1320 fn quantile_at_0975_is_near_196() {
1321 let q = standard_normal_quantile(0.975).unwrap();
1322 assert!((q - 1.959_963_985).abs() < 1e-7, "q={q}");
1323 }
1324
1325 #[test]
1326 fn quantile_antisymmetry() {
1327 let q_lo = standard_normal_quantile(0.1).unwrap();
1328 let q_hi = standard_normal_quantile(0.9).unwrap();
1329 assert!((q_lo + q_hi).abs() < 1e-10, "q_lo={q_lo} q_hi={q_hi}");
1330 }
1331
1332 #[test]
1333 fn quantile_roundtrip_cdf() {
1334 for &p in &[
1335 0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999,
1336 ] {
1337 let q = standard_normal_quantile(p).unwrap();
1338 let p_back = normal_cdf(q);
1339 assert!(
1340 (p_back - p).abs() < 1e-10,
1341 "roundtrip failed at p={p}: q={q} p_back={p_back}"
1342 );
1343 }
1344 }
1345}