1use statrs::function::{beta::inv_beta_reg, erf::erfc};
2
3pub fn beta_quantile(p: f64, a: f64, b: f64) -> f64 {
10 if !(a.is_finite() && a > 0.0 && b.is_finite() && b > 0.0) {
11 return f64::NAN;
12 }
13 if !p.is_finite() || p <= 0.0 {
14 return 0.0;
15 }
16 if p >= 1.0 {
17 return 1.0;
18 }
19 inv_beta_reg(a, b, p)
20}
21
22#[inline]
24pub fn normal_pdf(x: f64) -> f64 {
25 const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
26 INV_SQRT_2PI * (-0.5 * x * x).exp()
27}
28
29#[inline]
38pub fn normal_cdf(x: f64) -> f64 {
39 0.5 * statrs::function::erf::erfc(-x / std::f64::consts::SQRT_2)
40}
41
42#[inline]
50pub fn erfcx_nonnegative(x: f64) -> f64 {
51 if !x.is_finite() {
52 return if x.is_sign_positive() {
53 0.0
54 } else {
55 f64::INFINITY
56 };
57 }
58 if x <= 0.0 {
59 return 1.0;
60 }
61 if x < 26.0 {
62 ((x * x).min(700.0)).exp() * erfc(x)
63 } else {
64 let inv = 1.0 / x;
65 let inv2 = inv * inv;
66 let poly = 1.0 - 0.5 * inv2 + 0.75 * inv2 * inv2 - 1.875 * inv2 * inv2 * inv2
67 + 6.5625 * inv2 * inv2 * inv2 * inv2;
68 inv * poly / std::f64::consts::PI.sqrt()
69 }
70}
71
72#[inline]
74pub fn log1mexp_positive(a: f64) -> f64 {
75 assert!(a >= 0.0, "log1mexp_positive requires a >= 0: a={a}");
76 if a > core::f64::consts::LN_2 {
77 (-(-a).exp()).ln_1p()
78 } else if a > 0.0 {
79 (-(-a).exp_m1()).ln()
80 } else {
81 f64::NEG_INFINITY
82 }
83}
84
85pub fn signed_log_sum_exp(log_mags: &[f64], signs: &[f64]) -> (f64, f64) {
103 let mut has_pos_inf = false;
107 let mut has_neg_inf = false;
108 for (idx, &lm) in log_mags.iter().enumerate() {
109 if lm == f64::INFINITY {
110 if signs[idx] > 0.0 {
111 has_pos_inf = true;
112 } else if signs[idx] < 0.0 {
113 has_neg_inf = true;
114 }
115 }
116 }
117 match (has_pos_inf, has_neg_inf) {
118 (true, true) => return (f64::NAN, 0.0),
120 (true, false) => return (f64::INFINITY, 1.0),
122 (false, true) => return (f64::INFINITY, -1.0),
124 (false, false) => {}
125 }
126
127 let mut pos_max = f64::NEG_INFINITY;
128 let mut neg_max = f64::NEG_INFINITY;
129 for (idx, &lm) in log_mags.iter().enumerate() {
130 if signs[idx] > 0.0 {
131 pos_max = pos_max.max(lm);
132 } else if signs[idx] < 0.0 {
133 neg_max = neg_max.max(lm);
134 }
135 }
136
137 let mut pos_sum = 0.0_f64;
138 let mut neg_sum = 0.0_f64;
139 for (idx, &lm) in log_mags.iter().enumerate() {
140 if !lm.is_finite() {
141 continue;
142 }
143 if signs[idx] > 0.0 {
144 pos_sum += (lm - pos_max).exp();
145 } else if signs[idx] < 0.0 {
146 neg_sum += (lm - neg_max).exp();
147 }
148 }
149
150 let log_pos = if pos_sum > 0.0 {
151 pos_max + pos_sum.ln()
152 } else {
153 f64::NEG_INFINITY
154 };
155 let log_neg = if neg_sum > 0.0 {
156 neg_max + neg_sum.ln()
157 } else {
158 f64::NEG_INFINITY
159 };
160
161 if log_pos == f64::NEG_INFINITY && log_neg == f64::NEG_INFINITY {
162 return (f64::NEG_INFINITY, 0.0);
168 }
169 if log_neg == f64::NEG_INFINITY {
170 return (log_pos, 1.0);
171 }
172 if log_pos == f64::NEG_INFINITY {
173 return (log_neg, -1.0);
174 }
175 if log_pos > log_neg {
176 let gap = log_pos - log_neg;
177 (log_pos + log1mexp_positive(gap), 1.0)
178 } else if log_neg > log_pos {
179 let gap = log_neg - log_pos;
180 (log_neg + log1mexp_positive(gap), -1.0)
181 } else {
182 (f64::NEG_INFINITY, 0.0)
183 }
184}
185
186#[inline]
193pub fn normal_logcdf(x: f64) -> f64 {
194 if x == f64::INFINITY {
195 return 0.0;
196 }
197 if x == f64::NEG_INFINITY {
198 return f64::NEG_INFINITY;
199 }
200 if x.is_nan() {
201 return f64::NAN;
202 }
203 if x < 0.0 {
204 let u = -x / std::f64::consts::SQRT_2;
205 -u * u + (0.5 * erfcx_nonnegative(u).max(1e-300)).ln()
206 } else {
207 normal_cdf(x).clamp(1e-300, 1.0).ln()
208 }
209}
210
211#[inline]
215pub fn normal_logsf(x: f64) -> f64 {
216 normal_logcdf(-x)
217}
218
219#[inline]
226pub fn signed_probit_logcdf_and_mills_ratio(x: f64) -> (f64, f64) {
227 if x == f64::INFINITY {
228 return (0.0, 0.0);
229 }
230 if x == f64::NEG_INFINITY {
231 return (f64::NEG_INFINITY, f64::INFINITY);
232 }
233 if x.is_nan() {
234 return (f64::NAN, f64::NAN);
235 }
236 if x < 0.0 {
237 let u = -x / std::f64::consts::SQRT_2;
238 let ex = erfcx_nonnegative(u).max(1e-300);
239 let log_cdf = -u * u + (0.5 * ex).ln();
240 let lambda = (2.0 / std::f64::consts::PI).sqrt() / ex;
241 (log_cdf, lambda)
242 } else {
243 let cdf = normal_cdf(x).clamp(1e-300, 1.0);
244 let lambda = normal_pdf(x) / cdf;
245 (cdf.ln(), lambda)
246 }
247}
248
249#[inline]
251pub fn standard_normal_quantile(p: f64) -> Result<f64, String> {
252 if !(p.is_finite() && p > 0.0 && p < 1.0) {
253 return Err(format!("normal quantile requires p in (0,1), got {p}"));
254 }
255
256 const A: [f64; 6] = [
257 -3.969_683_028_665_376e1,
258 2.209_460_984_245_205e2,
259 -2.759_285_104_469_687e2,
260 1.383_577_518_672_69e2,
261 -3.066_479_806_614_716e1,
262 2.506_628_277_459_239,
263 ];
264 const B: [f64; 5] = [
265 -5.447_609_879_822_406e1,
266 1.615_858_368_580_409e2,
267 -1.556_989_798_598_866e2,
268 6.680_131_188_771_972e1,
269 -1.328_068_155_288_572e1,
270 ];
271 const C: [f64; 6] = [
272 -7.784_894_002_430_293e-3,
273 -3.223_964_580_411_365e-1,
274 -2.400_758_277_161_838,
275 -2.549_732_539_343_734,
276 4.374_664_141_464_968,
277 2.938_163_982_698_783,
278 ];
279 const D: [f64; 4] = [
280 7.784_695_709_041_462e-3,
281 3.224_671_290_700_398e-1,
282 2.445_134_137_142_996,
283 3.754_408_661_907_416,
284 ];
285 const P_LOW: f64 = 0.02425;
286 const P_HIGH: f64 = 1.0 - P_LOW;
287
288 let mut x = if p < P_LOW {
289 let q = (-2.0 * p.ln()).sqrt();
290 (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
291 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
292 } else if p <= P_HIGH {
293 let q = p - 0.5;
294 let r = q * q;
295 (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
296 / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
297 } else {
298 let q = (-2.0 * (1.0 - p).ln()).sqrt();
299 -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
300 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
301 };
302 for _ in 0..2 {
303 let density = normal_pdf(x);
304 if !(density.is_finite() && density > 0.0) {
305 break;
306 }
307 let residual = if x > 0.0 {
317 (1.0 - p) - 0.5 * erfc(x / std::f64::consts::SQRT_2)
318 } else {
319 normal_cdf(x) - p
320 };
321 let correction = residual / density;
322 let denominator = 1.0 + 0.5 * x * correction;
323 if !(correction.is_finite() && denominator.is_finite() && denominator != 0.0) {
324 break;
325 }
326 let step = correction / denominator;
327 if !step.is_finite() {
328 break;
329 }
330 x -= step;
331 if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
332 break;
333 }
334 }
335 Ok(x)
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 const TOL: f64 = 1e-12;
343
344 fn rel_err(got: f64, expected: f64) -> f64 {
345 (got - expected).abs() / expected.abs().max(1e-300)
346 }
347
348 #[test]
349 fn beta_quantile_matches_known_reference_values() {
350 let cases: [(f64, f64, f64, f64); 8] = [
351 (0.025, 2.0, 2.0, 0.094_299_3),
352 (0.975, 2.0, 2.0, 0.905_700_7),
353 (0.5, 2.0, 2.0, 0.5),
354 (0.025, 0.8, 4.0, 0.002_339_1),
355 (0.975, 0.8, 4.0, 0.564_717_3),
356 (0.025, 5.0, 1.5, 0.408_549_1),
357 (0.5, 20.0, 80.0, 0.197_994_8),
358 (0.975, 20.0, 80.0, 0.283_367_6),
359 ];
360 for (p, a, b, expected) in cases {
361 let got = beta_quantile(p, a, b);
362 let abs = (got - expected).abs();
363 assert!(
364 abs < 1e-5,
365 "beta_quantile(p={p}, a={a}, b={b}) = {got}, expected ≈ {expected} (abs err {abs})"
366 );
367 }
368 }
369
370 #[test]
371 fn beta_quantile_boundaries_and_degeneracy() {
372 assert_eq!(beta_quantile(0.0, 2.0, 3.0), 0.0);
373 assert_eq!(beta_quantile(-0.5, 2.0, 3.0), 0.0);
374 assert_eq!(beta_quantile(1.0, 2.0, 3.0), 1.0);
375 assert_eq!(beta_quantile(1.5, 2.0, 3.0), 1.0);
376 assert!(beta_quantile(0.5, -1.0, 3.0).is_nan());
377 assert!(beta_quantile(0.5, 2.0, 0.0).is_nan());
378 assert!(beta_quantile(0.5, f64::NAN, 3.0).is_nan());
379 let mut prev = 0.0;
380 for i in 1..100 {
381 let p = i as f64 / 100.0;
382 let q = beta_quantile(p, 3.0, 5.0);
383 assert!(q > prev, "beta quantile not increasing at p={p}");
384 prev = q;
385 }
386 }
387
388 #[test]
391 fn normal_pdf_at_zero() {
392 let expected = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
393 assert!((normal_pdf(0.0) - expected).abs() < TOL);
394 }
395
396 #[test]
397 fn normal_pdf_symmetry() {
398 for &x in &[0.5, 1.0, 2.0, 3.0, 5.0] {
399 assert_eq!(normal_pdf(x), normal_pdf(-x), "symmetry failed at x={x}");
400 }
401 }
402
403 #[test]
404 fn normal_pdf_positive() {
405 for &x in &[-5.0, -1.0, 0.0, 1.0, 5.0] {
406 assert!(normal_pdf(x) > 0.0, "pdf should be positive at x={x}");
407 }
408 }
409
410 #[test]
413 fn normal_cdf_at_zero_is_half() {
414 assert!((normal_cdf(0.0) - 0.5).abs() < TOL);
415 }
416
417 #[test]
418 fn normal_cdf_symmetry() {
419 for &x in &[0.5, 1.0, 2.0, 3.0] {
420 let sum = normal_cdf(x) + normal_cdf(-x);
421 assert!(
422 (sum - 1.0).abs() < TOL,
423 "cdf symmetry failed at x={x}: sum={sum}"
424 );
425 }
426 }
427
428 #[test]
429 fn normal_cdf_bounds() {
430 assert!(normal_cdf(10.0) > 0.9999);
431 assert!(normal_cdf(-10.0) < 1e-22);
432 assert!(normal_cdf(0.0) > 0.0);
433 assert!(normal_cdf(0.0) < 1.0);
434 }
435
436 #[test]
437 fn normal_cdf_at_1_96_near_0975() {
438 let p = normal_cdf(1.959_963_985);
440 assert!((p - 0.975).abs() < 1e-8, "p={p}");
441 }
442
443 #[test]
446 fn erfcx_at_nonpositive_returns_one() {
447 assert_eq!(erfcx_nonnegative(0.0), 1.0);
448 assert_eq!(erfcx_nonnegative(-1.0), 1.0);
449 assert_eq!(erfcx_nonnegative(-100.0), 1.0);
450 }
451
452 #[test]
453 fn erfcx_positive_inf_returns_zero() {
454 assert_eq!(erfcx_nonnegative(f64::INFINITY), 0.0);
455 }
456
457 #[test]
458 fn erfcx_negative_inf_returns_inf() {
459 assert_eq!(erfcx_nonnegative(f64::NEG_INFINITY), f64::INFINITY);
460 }
461
462 #[test]
463 fn erfcx_small_positive_matches_direct() {
464 use statrs::function::erf::erfc;
465 for &x in &[0.1_f64, 0.5, 1.0, 5.0, 10.0, 25.0] {
466 let got = erfcx_nonnegative(x);
467 let expected = (x * x).exp() * erfc(x);
468 let err = rel_err(got, expected);
469 assert!(
470 err < 1e-10,
471 "x={x}: got={got} expected={expected} rel={err}"
472 );
473 }
474 }
475
476 #[test]
477 fn erfcx_large_x_positive_and_finite() {
478 let got = erfcx_nonnegative(50.0);
480 assert!(got.is_finite() && got > 0.0, "erfcx(50)={got}");
481 let asymptotic = 1.0 / (50.0 * std::f64::consts::PI.sqrt());
483 assert!(
484 rel_err(got, asymptotic) < 1e-3,
485 "got={got} asymptotic={asymptotic}"
486 );
487 }
488
489 #[test]
492 fn log1mexp_at_zero_is_neg_inf() {
493 assert_eq!(log1mexp_positive(0.0), f64::NEG_INFINITY);
494 }
495
496 #[test]
497 fn log1mexp_recovers_log_one_minus_exp() {
498 for &a in &[0.001_f64, 0.5, std::f64::consts::LN_2, 1.0, 5.0, 20.0] {
503 let lm = log1mexp_positive(a);
504 let roundtrip = lm.exp() + (-a).exp();
505 assert!(
506 (roundtrip - 1.0).abs() < 1e-14,
507 "a={a}: exp(log1mexp(a)) + exp(-a) = {roundtrip}, expected 1.0"
508 );
509 }
510 }
511
512 #[test]
513 fn log1mexp_at_ln2_is_neg_ln2() {
514 let ln2 = std::f64::consts::LN_2;
515 let got = log1mexp_positive(ln2);
516 assert!((got - (-ln2)).abs() < TOL, "got={got}");
517 }
518
519 #[test]
522 fn slse_all_positive_single() {
523 let (lm, sg) = signed_log_sum_exp(&[2.0], &[1.0]);
524 assert!((lm - 2.0).abs() < TOL);
525 assert!((sg - 1.0).abs() < TOL);
526 }
527
528 #[test]
529 fn slse_difference_recovers_log2() {
530 let log3 = 3.0_f64.ln();
532 let log1 = 0.0_f64; let (lm, sg) = signed_log_sum_exp(&[log3, log1], &[1.0, -1.0]);
534 assert!((lm - 2.0_f64.ln()).abs() < TOL, "lm={lm}");
535 assert!((sg - 1.0).abs() < TOL, "sg={sg}");
536 }
537
538 #[test]
539 fn slse_cancellation_gives_neg_inf() {
540 let ln2 = 2.0_f64.ln();
542 let (lm, sg) = signed_log_sum_exp(&[ln2, ln2], &[1.0, -1.0]);
543 assert_eq!(lm, f64::NEG_INFINITY);
544 assert_eq!(sg, 0.0);
545 }
546
547 #[test]
548 fn slse_empty_returns_neg_inf_with_zero_sign() {
549 let (lm, sg) = signed_log_sum_exp(&[], &[]);
554 assert_eq!(lm, f64::NEG_INFINITY);
555 assert_eq!(sg, 0.0);
556 }
557
558 #[test]
559 fn slse_all_zero_signs_return_zero_sign() {
560 let (lm, sg) = signed_log_sum_exp(&[0.0], &[0.0]);
562 assert_eq!(lm, f64::NEG_INFINITY);
563 assert_eq!(sg, 0.0);
564 }
565
566 #[test]
567 fn slse_all_neg_inf_magnitudes_return_zero_sign() {
568 let (lm, sg) = signed_log_sum_exp(&[f64::NEG_INFINITY, f64::NEG_INFINITY], &[1.0, -1.0]);
571 assert_eq!(lm, f64::NEG_INFINITY);
572 assert_eq!(sg, 0.0);
573 }
574
575 #[test]
576 fn slse_pos_inf_dominates() {
577 let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[1.0, -1.0]);
578 assert_eq!(lm, f64::INFINITY);
579 assert_eq!(sg, 1.0);
580 }
581
582 #[test]
583 fn slse_neg_inf_dominates() {
584 let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[-1.0, 1.0]);
585 assert_eq!(lm, f64::INFINITY);
586 assert_eq!(sg, -1.0);
587 }
588
589 #[test]
590 fn slse_both_inf_signs_gives_nan() {
591 let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, f64::INFINITY], &[1.0, -1.0]);
592 assert!(lm.is_nan());
593 assert_eq!(sg, 0.0);
594 }
595
596 #[test]
599 fn logcdf_at_zero_is_log_half() {
600 let got = normal_logcdf(0.0);
601 let expected = 0.5_f64.ln();
602 assert!((got - expected).abs() < TOL, "got={got}");
603 }
604
605 #[test]
606 fn logcdf_pos_inf_is_zero() {
607 assert_eq!(normal_logcdf(f64::INFINITY), 0.0);
608 }
609
610 #[test]
611 fn logcdf_neg_inf_is_neg_inf() {
612 assert_eq!(normal_logcdf(f64::NEG_INFINITY), f64::NEG_INFINITY);
613 }
614
615 #[test]
616 fn logcdf_nan_is_nan() {
617 assert!(normal_logcdf(f64::NAN).is_nan());
618 }
619
620 #[test]
621 fn logcdf_matches_log_cdf_for_moderate_x() {
622 for &x in &[-2.0_f64, -1.0, 0.0, 1.0, 2.0, 3.0] {
623 let got = normal_logcdf(x);
624 let expected = normal_cdf(x).ln();
625 assert!(
626 (got - expected).abs() < 1e-10,
627 "x={x}: got={got} expected={expected}"
628 );
629 }
630 }
631
632 #[test]
633 fn logcdf_deep_left_tail_stays_finite() {
634 let got = normal_logcdf(-20.0);
637 assert!(got.is_finite() && got < -100.0, "logcdf(-20)={got}");
638 }
639
640 #[test]
643 fn logsf_at_zero_is_log_half() {
644 let got = normal_logsf(0.0);
645 let expected = 0.5_f64.ln();
646 assert!((got - expected).abs() < TOL, "got={got}");
647 }
648
649 #[test]
650 fn logsf_mirrors_logcdf() {
651 for &x in &[-3.0_f64, -1.0, 0.0, 1.0, 3.0] {
653 assert_eq!(normal_logsf(x), normal_logcdf(-x));
654 }
655 }
656
657 #[test]
660 fn probit_at_pos_inf() {
661 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::INFINITY);
662 assert_eq!(lc, 0.0);
663 assert_eq!(mr, 0.0);
664 }
665
666 #[test]
667 fn probit_at_neg_inf() {
668 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NEG_INFINITY);
669 assert_eq!(lc, f64::NEG_INFINITY);
670 assert_eq!(mr, f64::INFINITY);
671 }
672
673 #[test]
674 fn probit_nan_propagates() {
675 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NAN);
676 assert!(lc.is_nan() && mr.is_nan());
677 }
678
679 #[test]
680 fn probit_at_zero_logcdf_and_mills() {
681 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(0.0);
682 assert!((lc - 0.5_f64.ln()).abs() < TOL, "lc={lc}");
683 assert!((mr - 0.797_884_560_802_865).abs() < 1e-10, "mr={mr}");
685 }
686
687 #[test]
688 fn probit_positive_branch_matches_logcdf() {
689 for &x in &[0.5_f64, 1.0, 2.0, 3.0] {
690 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
691 let lc_ref = normal_logcdf(x);
692 let mr_ref = normal_pdf(x) / normal_cdf(x);
693 assert!(
694 (lc - lc_ref).abs() < 1e-10,
695 "x={x}: lc={lc} lc_ref={lc_ref}"
696 );
697 assert!(
698 (mr - mr_ref).abs() < 1e-10,
699 "x={x}: mr={mr} mr_ref={mr_ref}"
700 );
701 }
702 }
703
704 #[test]
705 fn probit_negative_branch_matches_logcdf() {
706 for &x in &[-0.5_f64, -1.0, -2.0, -5.0] {
707 let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
708 let lc_ref = normal_logcdf(x);
709 assert!(
710 (lc - lc_ref).abs() < 1e-10,
711 "x={x}: lc={lc} lc_ref={lc_ref}"
712 );
713 assert!(mr.is_finite() && mr > 0.0, "x={x}: mr={mr}");
714 }
715 }
716
717 #[test]
720 fn quantile_rejects_out_of_range() {
721 assert!(standard_normal_quantile(0.0).is_err());
722 assert!(standard_normal_quantile(1.0).is_err());
723 assert!(standard_normal_quantile(-0.1).is_err());
724 assert!(standard_normal_quantile(1.1).is_err());
725 assert!(standard_normal_quantile(f64::NAN).is_err());
726 }
727
728 #[test]
729 fn quantile_at_half_is_near_zero() {
730 let q = standard_normal_quantile(0.5).unwrap();
731 assert!(q.abs() < 1e-10, "quantile(0.5)={q}");
732 }
733
734 #[test]
735 fn quantile_at_0975_is_near_196() {
736 let q = standard_normal_quantile(0.975).unwrap();
737 assert!((q - 1.959_963_985).abs() < 1e-7, "q={q}");
738 }
739
740 #[test]
741 fn quantile_antisymmetry() {
742 let q_lo = standard_normal_quantile(0.1).unwrap();
743 let q_hi = standard_normal_quantile(0.9).unwrap();
744 assert!((q_lo + q_hi).abs() < 1e-10, "q_lo={q_lo} q_hi={q_hi}");
745 }
746
747 #[test]
748 fn quantile_roundtrip_cdf() {
749 for &p in &[
750 0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999,
751 ] {
752 let q = standard_normal_quantile(p).unwrap();
753 let p_back = normal_cdf(q);
754 assert!(
755 (p_back - p).abs() < 1e-10,
756 "roundtrip failed at p={p}: q={q} p_back={p_back}"
757 );
758 }
759 }
760}