1use bicmath_core::error::{EngineError, ErrorCode};
10
11const EPS: f64 = 1e-16;
12const FPMIN: f64 = f64::MIN_POSITIVE / EPS;
13const MAX_ITER: u64 = 100_000;
14const LN_2_PI: f64 = 1.837_877_066_409_345_3;
15const SQRT_2PI: f64 = 2.506_628_274_631_000_7;
16const SQRT_2: f64 = std::f64::consts::SQRT_2;
17
18#[cfg(target_arch = "wasm32")]
23pub fn exp(x: f64) -> f64 {
24 libm::exp(x)
25}
26#[cfg(not(target_arch = "wasm32"))]
27pub fn exp(x: f64) -> f64 {
28 x.exp()
29}
30
31#[cfg(target_arch = "wasm32")]
32pub fn ln(x: f64) -> f64 {
33 libm::log(x)
34}
35#[cfg(not(target_arch = "wasm32"))]
36pub fn ln(x: f64) -> f64 {
37 x.ln()
38}
39
40#[cfg(target_arch = "wasm32")]
41pub fn sqrt(x: f64) -> f64 {
42 libm::sqrt(x)
43}
44#[cfg(not(target_arch = "wasm32"))]
45pub fn sqrt(x: f64) -> f64 {
46 x.sqrt()
47}
48
49#[cfg(target_arch = "wasm32")]
50pub fn sin(x: f64) -> f64 {
51 libm::sin(x)
52}
53#[cfg(not(target_arch = "wasm32"))]
54pub fn sin(x: f64) -> f64 {
55 x.sin()
56}
57
58#[cfg(target_arch = "wasm32")]
59pub fn abs(x: f64) -> f64 {
60 libm::fabs(x)
61}
62#[cfg(not(target_arch = "wasm32"))]
63pub fn abs(x: f64) -> f64 {
64 x.abs()
65}
66
67fn non_convergence(what: &str) -> EngineError {
68 EngineError::new(
69 ErrorCode::NonConvergence,
70 format!("{what} did not converge within {MAX_ITER} iterations"),
71 )
72}
73
74const LANCZOS: [f64; 9] = [
79 0.999_999_999_999_809_9,
80 676.520_368_121_885_1,
81 -1_259.139_216_722_402_8,
82 771.323_428_777_653_1,
83 -176.615_029_162_140_6,
84 12.507_343_278_686_905,
85 -0.138_571_095_265_720_12,
86 9.984_369_578_019_572e-6,
87 1.505_632_735_149_311_6e-7,
88];
89
90pub fn lgamma(x: f64) -> f64 {
92 if x < 0.5 {
93 let pi = std::f64::consts::PI;
95 return ln(pi / abs(sin(pi * x))) - lgamma(1.0 - x);
96 }
97 let z = x - 1.0;
98 let mut series = LANCZOS[0];
99 for (index, coefficient) in LANCZOS.iter().enumerate().skip(1) {
100 series += coefficient / (z + index as f64);
101 }
102 let t = z + 7.5;
103 0.5 * LN_2_PI + (z + 0.5) * ln(t) - t + ln(series)
104}
105
106pub fn gamma_p(a: f64, x: f64) -> Result<f64, EngineError> {
112 if a <= 0.0 || !a.is_finite() {
113 return Err(EngineError::domain("incomplete gamma requires a > 0"));
114 }
115 if x <= 0.0 {
116 return Ok(0.0);
117 }
118 if x < a + 1.0 {
119 gamma_p_series(a, x)
120 } else {
121 Ok(1.0 - gamma_q_cf(a, x)?)
122 }
123}
124
125pub fn gamma_q(a: f64, x: f64) -> Result<f64, EngineError> {
127 if a <= 0.0 || !a.is_finite() {
128 return Err(EngineError::domain("incomplete gamma requires a > 0"));
129 }
130 if x <= 0.0 {
131 return Ok(1.0);
132 }
133 if x < a + 1.0 {
134 Ok(1.0 - gamma_p_series(a, x)?)
135 } else {
136 gamma_q_cf(a, x)
137 }
138}
139
140fn gamma_p_series(a: f64, x: f64) -> Result<f64, EngineError> {
141 let mut ap = a;
142 let mut sum = 1.0 / a;
143 let mut del = sum;
144 let mut iterations = 0u64;
145 loop {
146 iterations += 1;
147 if iterations > MAX_ITER {
148 return Err(non_convergence("incomplete gamma series"));
149 }
150 ap += 1.0;
151 del *= x / ap;
152 sum += del;
153 if abs(del) < abs(sum) * EPS {
154 break;
155 }
156 }
157 let prefactor = exp(-x + a * ln(x) - lgamma(a));
158 Ok(sum * prefactor)
159}
160
161fn gamma_q_cf(a: f64, x: f64) -> Result<f64, EngineError> {
162 let mut b = x + 1.0 - a;
163 let mut c = 1.0 / FPMIN;
164 let mut d = 1.0 / b;
165 let mut h = d;
166 let mut iterations = 0u64;
167 loop {
168 iterations += 1;
169 if iterations > MAX_ITER {
170 return Err(non_convergence("incomplete gamma continued fraction"));
171 }
172 let an = -(iterations as f64) * (iterations as f64 - a);
173 b += 2.0;
174 d = an * d + b;
175 if abs(d) < FPMIN {
176 d = FPMIN;
177 }
178 c = b + an / c;
179 if abs(c) < FPMIN {
180 c = FPMIN;
181 }
182 d = 1.0 / d;
183 let delta = d * c;
184 h *= delta;
185 if abs(delta - 1.0) < EPS {
186 break;
187 }
188 }
189 let prefactor = exp(-x + a * ln(x) - lgamma(a));
190 Ok(prefactor * h)
191}
192
193pub fn ibeta(a: f64, b: f64, x: f64) -> Result<f64, EngineError> {
199 if a <= 0.0 || b <= 0.0 || !a.is_finite() || !b.is_finite() {
200 return Err(EngineError::domain(
201 "incomplete beta requires finite positive shape parameters",
202 ));
203 }
204 if x <= 0.0 {
205 return Ok(0.0);
206 }
207 if x >= 1.0 {
208 return Ok(1.0);
209 }
210 let front = exp(lgamma(a + b) - lgamma(a) - lgamma(b) + a * ln(x) + b * ln(1.0 - x));
211 if x < (a + 1.0) / (a + b + 2.0) {
212 Ok(front * betacf(a, b, x)? / a)
213 } else {
214 Ok(1.0 - front * betacf(b, a, 1.0 - x)? / b)
215 }
216}
217
218fn betacf(a: f64, b: f64, x: f64) -> Result<f64, EngineError> {
219 let qab = a + b;
220 let qap = a + 1.0;
221 let qam = a - 1.0;
222 let mut c = 1.0;
223 let mut d = 1.0 - qab * x / qap;
224 if abs(d) < FPMIN {
225 d = FPMIN;
226 }
227 d = 1.0 / d;
228 let mut h = d;
229 let mut m = 0u64;
230 loop {
231 m += 1;
232 if m > MAX_ITER {
233 return Err(non_convergence("incomplete beta continued fraction"));
234 }
235 let mf = m as f64;
236 let m2 = 2.0 * mf;
237 let aa = mf * (b - mf) * x / ((qam + m2) * (a + m2));
238 d = 1.0 + aa * d;
239 if abs(d) < FPMIN {
240 d = FPMIN;
241 }
242 c = 1.0 + aa / c;
243 if abs(c) < FPMIN {
244 c = FPMIN;
245 }
246 d = 1.0 / d;
247 h *= d * c;
248 let aa = -(a + mf) * (qab + mf) * x / ((a + m2) * (qap + m2));
249 d = 1.0 + aa * d;
250 if abs(d) < FPMIN {
251 d = FPMIN;
252 }
253 c = 1.0 + aa / c;
254 if abs(c) < FPMIN {
255 c = FPMIN;
256 }
257 d = 1.0 / d;
258 let delta = d * c;
259 h *= delta;
260 if abs(delta - 1.0) < EPS {
261 return Ok(h);
262 }
263 }
264}
265
266pub fn erf(x: f64) -> Result<f64, EngineError> {
273 if x == 0.0 {
274 return Ok(0.0);
275 }
276 let magnitude = gamma_p(0.5, x * x)?;
277 Ok(if x > 0.0 { magnitude } else { -magnitude })
278}
279
280pub fn erfc(x: f64) -> Result<f64, EngineError> {
283 if x >= 0.0 {
284 gamma_q(0.5, x * x)
285 } else {
286 Ok(2.0 - gamma_q(0.5, x * x)?)
287 }
288}
289
290fn normal_pdf_std(x: f64) -> f64 {
295 exp(-0.5 * x * x) / SQRT_2PI
296}
297
298pub fn normal_pdf(x: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
299 if !x.is_finite() || !mean.is_finite() {
300 return Err(EngineError::domain("normal_pdf requires finite inputs"));
301 }
302 if sd <= 0.0 || !sd.is_finite() {
303 return Err(EngineError::domain("normal_pdf requires sd > 0"));
304 }
305 let z = (x - mean) / sd;
306 Ok(normal_pdf_std(z) / sd)
307}
308
309pub fn normal_cdf(x: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
310 if !x.is_finite() || !mean.is_finite() {
311 return Err(EngineError::domain("normal_cdf requires finite inputs"));
312 }
313 if sd <= 0.0 || !sd.is_finite() {
314 return Err(EngineError::domain("normal_cdf requires sd > 0"));
315 }
316 Ok(0.5 * erfc(-(x - mean) / (sd * SQRT_2))?)
317}
318
319pub fn normal_sf(x: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
322 if !x.is_finite() || !mean.is_finite() {
323 return Err(EngineError::domain("normal_sf requires finite inputs"));
324 }
325 if sd <= 0.0 || !sd.is_finite() {
326 return Err(EngineError::domain("normal_sf requires sd > 0"));
327 }
328 Ok(0.5 * erfc((x - mean) / (sd * SQRT_2))?)
329}
330
331pub fn normal_quantile(p: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
332 if !(0.0..1.0).contains(&p) {
333 return Err(EngineError::domain(
334 "normal_quantile requires p strictly between 0 and 1",
335 ));
336 }
337 if !mean.is_finite() || sd <= 0.0 || !sd.is_finite() {
338 return Err(EngineError::domain(
339 "normal_quantile requires finite mean and sd > 0",
340 ));
341 }
342 if p == 0.5 {
343 return Ok(mean);
344 }
345 let z = invert_monotone(
346 p,
347 -1.0,
348 1.0,
349 &mut |x| normal_cdf(x, 0.0, 1.0),
350 &mut normal_pdf_std,
351 )?;
352 Ok(mean + sd * z)
353}
354
355fn student_t_pdf_raw(x: f64, df: f64) -> f64 {
360 let half = 0.5 * df;
361 let ln_pdf = lgamma(half + 0.5)
362 - lgamma(half)
363 - 0.5 * ln(df * std::f64::consts::PI)
364 - (half + 0.5) * ln(1.0 + x * x / df);
365 exp(ln_pdf)
366}
367
368pub fn student_t_pdf(x: f64, df: f64) -> Result<f64, EngineError> {
369 if !x.is_finite() {
370 return Err(EngineError::domain("student_t_pdf requires a finite x"));
371 }
372 if df <= 0.0 || !df.is_finite() {
373 return Err(EngineError::domain("student_t_pdf requires df > 0"));
374 }
375 Ok(student_t_pdf_raw(x, df))
376}
377
378pub fn student_t_cdf(x: f64, df: f64) -> Result<f64, EngineError> {
379 if !x.is_finite() {
380 return Err(EngineError::domain("student_t_cdf requires a finite x"));
381 }
382 if df <= 0.0 || !df.is_finite() {
383 return Err(EngineError::domain("student_t_cdf requires df > 0"));
384 }
385 if x == 0.0 {
386 return Ok(0.5);
387 }
388 let y = df / (df + x * x);
389 let tail = ibeta(0.5 * df, 0.5, y)?;
390 if x > 0.0 {
391 Ok(1.0 - 0.5 * tail)
392 } else {
393 Ok(0.5 * tail)
394 }
395}
396
397pub fn student_t_sf(x: f64, df: f64) -> Result<f64, EngineError> {
398 if !x.is_finite() {
399 return Err(EngineError::domain("student_t_sf requires a finite x"));
400 }
401 if df <= 0.0 || !df.is_finite() {
402 return Err(EngineError::domain("student_t_sf requires df > 0"));
403 }
404 if x == 0.0 {
405 return Ok(0.5);
406 }
407 let y = df / (df + x * x);
408 let tail = ibeta(0.5 * df, 0.5, y)?;
409 if x > 0.0 {
410 Ok(0.5 * tail)
411 } else {
412 Ok(1.0 - 0.5 * tail)
413 }
414}
415
416pub fn student_t_quantile(p: f64, df: f64) -> Result<f64, EngineError> {
417 if !(0.0..1.0).contains(&p) {
418 return Err(EngineError::domain(
419 "student_t_quantile requires p strictly between 0 and 1",
420 ));
421 }
422 if df <= 0.0 || !df.is_finite() {
423 return Err(EngineError::domain("student_t_quantile requires df > 0"));
424 }
425 if p == 0.5 {
426 return Ok(0.0);
427 }
428 invert_monotone(p, -1.0, 1.0, &mut |x| student_t_cdf(x, df), &mut |x| {
429 student_t_pdf_raw(x, df)
430 })
431}
432
433pub fn binomial_pmf(k: f64, n: f64, p: f64) -> Result<f64, EngineError> {
438 if !k.is_finite() || !n.is_finite() || !p.is_finite() {
439 return Err(EngineError::domain("binomial_pmf requires finite inputs"));
440 }
441 if !(0.0..=1.0).contains(&p) {
442 return Err(EngineError::domain("binomial_pmf requires p in [0, 1]"));
443 }
444 if k < 0.0 || k > n {
445 return Ok(0.0);
446 }
447 if p == 0.0 {
448 return Ok(if k == 0.0 { 1.0 } else { 0.0 });
449 }
450 if p == 1.0 {
451 return Ok(if k == n { 1.0 } else { 0.0 });
452 }
453 if k == 0.0 {
454 return Ok(exp(n * ln(1.0 - p)));
455 }
456 if k == n {
457 return Ok(exp(n * ln(p)));
458 }
459 let ln_pmf =
460 lgamma(n + 1.0) - lgamma(k + 1.0) - lgamma(n - k + 1.0) + k * ln(p) + (n - k) * ln(1.0 - p);
461 Ok(exp(ln_pmf))
462}
463
464pub fn binomial_cdf(k: f64, n: f64, p: f64) -> Result<f64, EngineError> {
465 if !k.is_finite() || !n.is_finite() || !p.is_finite() {
466 return Err(EngineError::domain("binomial_cdf requires finite inputs"));
467 }
468 if !(0.0..=1.0).contains(&p) {
469 return Err(EngineError::domain("binomial_cdf requires p in [0, 1]"));
470 }
471 if k < 0.0 {
472 return Ok(0.0);
473 }
474 if k >= n {
475 return Ok(1.0);
476 }
477 ibeta(n - k, k + 1.0, 1.0 - p)
478}
479
480pub fn chi_square_pdf(x: f64, df: f64) -> Result<f64, EngineError> {
485 if !x.is_finite() {
486 return Err(EngineError::domain("chi_square_pdf requires a finite x"));
487 }
488 if df <= 0.0 || !df.is_finite() {
489 return Err(EngineError::domain("chi_square_pdf requires df > 0"));
490 }
491 if x < 0.0 {
492 return Ok(0.0);
493 }
494 if x == 0.0 {
495 return match df.partial_cmp(&2.0) {
496 Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
497 "chi_square_pdf is unbounded at x = 0 for df < 2",
498 )),
499 Some(std::cmp::Ordering::Equal) => Ok(0.5),
500 _ => Ok(0.0),
501 };
502 }
503 let half = 0.5 * df;
504 let ln_pdf = (half - 1.0) * ln(x) - 0.5 * x - half * ln(2.0) - lgamma(half);
505 Ok(exp(ln_pdf))
506}
507
508pub fn chi_square_cdf(x: f64, df: f64) -> Result<f64, EngineError> {
509 if !x.is_finite() {
510 return Err(EngineError::domain("chi_square_cdf requires a finite x"));
511 }
512 if df <= 0.0 || !df.is_finite() {
513 return Err(EngineError::domain("chi_square_cdf requires df > 0"));
514 }
515 if x <= 0.0 {
516 return Ok(0.0);
517 }
518 gamma_p(0.5 * df, 0.5 * x)
519}
520
521pub fn chi_square_sf(x: f64, df: f64) -> Result<f64, EngineError> {
522 if !x.is_finite() {
523 return Err(EngineError::domain("chi_square_sf requires a finite x"));
524 }
525 if df <= 0.0 || !df.is_finite() {
526 return Err(EngineError::domain("chi_square_sf requires df > 0"));
527 }
528 if x <= 0.0 {
529 return Ok(1.0);
530 }
531 gamma_q(0.5 * df, 0.5 * x)
532}
533
534pub fn chi_square_quantile(p: f64, df: f64) -> Result<f64, EngineError> {
535 if !(0.0..1.0).contains(&p) {
536 return Err(EngineError::domain(
537 "chi_square_quantile requires p in [0, 1)",
538 ));
539 }
540 if df <= 0.0 || !df.is_finite() {
541 return Err(EngineError::domain("chi_square_quantile requires df > 0"));
542 }
543 if p == 0.0 {
544 return Ok(0.0);
545 }
546 invert_monotone(p, 0.0, 1.0, &mut |x| chi_square_cdf(x, df), &mut |x| {
547 chi_square_pdf_raw(x, df)
548 })
549}
550
551fn chi_square_pdf_raw(x: f64, df: f64) -> f64 {
552 if x <= 0.0 {
553 return 0.0;
554 }
555 let half = 0.5 * df;
556 exp((half - 1.0) * ln(x) - 0.5 * x - half * ln(2.0) - lgamma(half))
557}
558
559pub fn poisson_pmf(k: f64, lambda: f64) -> Result<f64, EngineError> {
564 if !k.is_finite() || !lambda.is_finite() {
565 return Err(EngineError::domain("poisson_pmf requires finite inputs"));
566 }
567 if lambda <= 0.0 {
568 return Err(EngineError::domain("poisson_pmf requires lambda > 0"));
569 }
570 if k < 0.0 {
571 return Ok(0.0);
572 }
573 if k == 0.0 {
574 return Ok(exp(-lambda));
575 }
576 let ln_pmf = -lambda + k * ln(lambda) - lgamma(k + 1.0);
577 Ok(exp(ln_pmf))
578}
579
580pub fn poisson_cdf(k: f64, lambda: f64) -> Result<f64, EngineError> {
581 if !k.is_finite() || !lambda.is_finite() {
582 return Err(EngineError::domain("poisson_cdf requires finite inputs"));
583 }
584 if lambda <= 0.0 {
585 return Err(EngineError::domain("poisson_cdf requires lambda > 0"));
586 }
587 if k < 0.0 {
588 return Ok(0.0);
589 }
590 gamma_q(k + 1.0, lambda)
591}
592
593pub fn exponential_pdf(x: f64, rate: f64) -> Result<f64, EngineError> {
598 if !x.is_finite() || !rate.is_finite() {
599 return Err(EngineError::domain(
600 "exponential_pdf requires finite inputs",
601 ));
602 }
603 if rate <= 0.0 {
604 return Err(EngineError::domain("exponential_pdf requires rate > 0"));
605 }
606 if x < 0.0 {
607 return Ok(0.0);
608 }
609 Ok(rate * exp(-rate * x))
610}
611
612pub fn exponential_cdf(x: f64, rate: f64) -> Result<f64, EngineError> {
613 if !x.is_finite() || !rate.is_finite() {
614 return Err(EngineError::domain(
615 "exponential_cdf requires finite inputs",
616 ));
617 }
618 if rate <= 0.0 {
619 return Err(EngineError::domain("exponential_cdf requires rate > 0"));
620 }
621 if x <= 0.0 {
622 return Ok(0.0);
623 }
624 Ok(1.0 - exp(-rate * x))
625}
626
627pub fn exponential_sf(x: f64, rate: f64) -> Result<f64, EngineError> {
630 if !x.is_finite() || !rate.is_finite() {
631 return Err(EngineError::domain("exponential_sf requires finite inputs"));
632 }
633 if rate <= 0.0 {
634 return Err(EngineError::domain("exponential_sf requires rate > 0"));
635 }
636 if x <= 0.0 {
637 return Ok(1.0);
638 }
639 Ok(exp(-rate * x))
640}
641
642pub fn exponential_quantile(p: f64, rate: f64) -> Result<f64, EngineError> {
643 if !(0.0..1.0).contains(&p) {
644 return Err(EngineError::domain(
645 "exponential_quantile requires p in [0, 1)",
646 ));
647 }
648 if rate <= 0.0 || !rate.is_finite() {
649 return Err(EngineError::domain(
650 "exponential_quantile requires rate > 0",
651 ));
652 }
653 if p == 0.0 {
654 return Ok(0.0);
655 }
656 Ok(-ln(1.0 - p) / rate)
657}
658
659fn uniform_bounds(lower: f64, upper: f64) -> Result<(), EngineError> {
664 if !lower.is_finite() || !upper.is_finite() {
665 return Err(EngineError::domain("uniform bounds must be finite"));
666 }
667 if upper <= lower {
668 return Err(EngineError::domain("uniform requires upper > lower"));
669 }
670 Ok(())
671}
672
673pub fn uniform_pdf(x: f64, lower: f64, upper: f64) -> Result<f64, EngineError> {
674 if !x.is_finite() {
675 return Err(EngineError::domain("uniform_pdf requires a finite x"));
676 }
677 uniform_bounds(lower, upper)?;
678 if x < lower || x > upper {
679 return Ok(0.0);
680 }
681 Ok(1.0 / (upper - lower))
682}
683
684pub fn uniform_cdf(x: f64, lower: f64, upper: f64) -> Result<f64, EngineError> {
685 if !x.is_finite() {
686 return Err(EngineError::domain("uniform_cdf requires a finite x"));
687 }
688 uniform_bounds(lower, upper)?;
689 Ok(((x - lower) / (upper - lower)).clamp(0.0, 1.0))
690}
691
692pub fn uniform_quantile(p: f64, lower: f64, upper: f64) -> Result<f64, EngineError> {
693 if !(0.0..=1.0).contains(&p) {
694 return Err(EngineError::domain("uniform_quantile requires p in [0, 1]"));
695 }
696 uniform_bounds(lower, upper)?;
697 Ok(lower + p * (upper - lower))
698}
699
700pub fn lognormal_pdf(x: f64, mu: f64, sigma: f64) -> Result<f64, EngineError> {
705 if !x.is_finite() || !mu.is_finite() || !sigma.is_finite() {
706 return Err(EngineError::domain("lognormal_pdf requires finite inputs"));
707 }
708 if sigma <= 0.0 {
709 return Err(EngineError::domain("lognormal_pdf requires sigma > 0"));
710 }
711 if x <= 0.0 {
712 return Ok(0.0);
713 }
714 let z = (ln(x) - mu) / sigma;
715 Ok(exp(-0.5 * z * z) / (x * sigma * SQRT_2PI))
716}
717
718pub fn lognormal_cdf(x: f64, mu: f64, sigma: f64) -> Result<f64, EngineError> {
719 if !x.is_finite() || !mu.is_finite() || !sigma.is_finite() {
720 return Err(EngineError::domain("lognormal_cdf requires finite inputs"));
721 }
722 if sigma <= 0.0 {
723 return Err(EngineError::domain("lognormal_cdf requires sigma > 0"));
724 }
725 if x <= 0.0 {
726 return Ok(0.0);
727 }
728 normal_cdf((ln(x) - mu) / sigma, 0.0, 1.0)
729}
730
731pub fn lognormal_quantile(p: f64, mu: f64, sigma: f64) -> Result<f64, EngineError> {
732 if !(0.0..1.0).contains(&p) {
733 return Err(EngineError::domain(
734 "lognormal_quantile requires p strictly between 0 and 1",
735 ));
736 }
737 if !mu.is_finite() || sigma <= 0.0 || !sigma.is_finite() {
738 return Err(EngineError::domain(
739 "lognormal_quantile requires finite mu and sigma > 0",
740 ));
741 }
742 let z = normal_quantile(p, 0.0, 1.0)?;
743 Ok(exp(mu + sigma * z))
744}
745
746fn gamma_pdf_raw(x: f64, shape: f64, rate: f64) -> f64 {
751 if x <= 0.0 {
752 return 0.0;
753 }
754 exp(shape * ln(rate) + (shape - 1.0) * ln(x) - rate * x - lgamma(shape))
755}
756
757pub fn gamma_pdf(x: f64, shape: f64, rate: f64) -> Result<f64, EngineError> {
758 if !x.is_finite() || !shape.is_finite() || !rate.is_finite() {
759 return Err(EngineError::domain("gamma_pdf requires finite inputs"));
760 }
761 if shape <= 0.0 {
762 return Err(EngineError::domain("gamma_pdf requires shape > 0"));
763 }
764 if rate <= 0.0 {
765 return Err(EngineError::domain("gamma_pdf requires rate > 0"));
766 }
767 if x < 0.0 {
768 return Ok(0.0);
769 }
770 if x == 0.0 {
771 return match shape.partial_cmp(&1.0) {
772 Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
773 "gamma_pdf is unbounded at x = 0 for shape < 1",
774 )),
775 Some(std::cmp::Ordering::Equal) => Ok(rate),
776 _ => Ok(0.0),
777 };
778 }
779 Ok(gamma_pdf_raw(x, shape, rate))
780}
781
782pub fn gamma_cdf(x: f64, shape: f64, rate: f64) -> Result<f64, EngineError> {
783 if !x.is_finite() || !shape.is_finite() || !rate.is_finite() {
784 return Err(EngineError::domain("gamma_cdf requires finite inputs"));
785 }
786 if shape <= 0.0 {
787 return Err(EngineError::domain("gamma_cdf requires shape > 0"));
788 }
789 if rate <= 0.0 {
790 return Err(EngineError::domain("gamma_cdf requires rate > 0"));
791 }
792 if x <= 0.0 {
793 return Ok(0.0);
794 }
795 gamma_p(shape, rate * x)
796}
797
798pub fn gamma_quantile(p: f64, shape: f64, rate: f64) -> Result<f64, EngineError> {
799 if !(0.0..1.0).contains(&p) {
800 return Err(EngineError::domain("gamma_quantile requires p in [0, 1)"));
801 }
802 if shape <= 0.0 || !shape.is_finite() {
803 return Err(EngineError::domain("gamma_quantile requires shape > 0"));
804 }
805 if rate <= 0.0 || !rate.is_finite() {
806 return Err(EngineError::domain("gamma_quantile requires rate > 0"));
807 }
808 if p == 0.0 {
809 return Ok(0.0);
810 }
811 invert_monotone(p, 0.0, 1.0, &mut |x| gamma_cdf(x, shape, rate), &mut |x| {
812 gamma_pdf_raw(x, shape, rate)
813 })
814}
815
816fn beta_pdf_raw(x: f64, alpha: f64, beta: f64) -> f64 {
821 exp((alpha - 1.0) * ln(x) + (beta - 1.0) * ln(1.0 - x)
822 - (lgamma(alpha) + lgamma(beta) - lgamma(alpha + beta)))
823}
824
825fn beta_shapes(alpha: f64, beta: f64) -> Result<(), EngineError> {
826 if alpha <= 0.0 || beta <= 0.0 || !alpha.is_finite() || !beta.is_finite() {
827 return Err(EngineError::domain(
828 "beta distribution requires finite positive shape parameters",
829 ));
830 }
831 Ok(())
832}
833
834pub fn beta_pdf(x: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
835 if !x.is_finite() {
836 return Err(EngineError::domain("beta_pdf requires a finite x"));
837 }
838 beta_shapes(alpha, beta)?;
839 if !(0.0..=1.0).contains(&x) {
840 return Ok(0.0);
841 }
842 if x == 0.0 {
843 return match alpha.partial_cmp(&1.0) {
844 Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
845 "beta_pdf is unbounded at x = 0 for alpha < 1",
846 )),
847 Some(std::cmp::Ordering::Equal) => Ok(beta),
848 _ => Ok(0.0),
849 };
850 }
851 if x == 1.0 {
852 return match beta.partial_cmp(&1.0) {
853 Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
854 "beta_pdf is unbounded at x = 1 for beta < 1",
855 )),
856 Some(std::cmp::Ordering::Equal) => Ok(alpha),
857 _ => Ok(0.0),
858 };
859 }
860 Ok(beta_pdf_raw(x, alpha, beta))
861}
862
863pub fn beta_cdf(x: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
864 if !x.is_finite() {
865 return Err(EngineError::domain("beta_cdf requires a finite x"));
866 }
867 beta_shapes(alpha, beta)?;
868 if x <= 0.0 {
869 return Ok(0.0);
870 }
871 if x >= 1.0 {
872 return Ok(1.0);
873 }
874 ibeta(alpha, beta, x)
875}
876
877pub fn beta_sf(x: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
880 if !x.is_finite() {
881 return Err(EngineError::domain("beta_sf requires a finite x"));
882 }
883 beta_shapes(alpha, beta)?;
884 if x <= 0.0 {
885 return Ok(1.0);
886 }
887 if x >= 1.0 {
888 return Ok(0.0);
889 }
890 ibeta(beta, alpha, 1.0 - x)
891}
892
893pub fn beta_quantile(p: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
894 if !(0.0..=1.0).contains(&p) {
895 return Err(EngineError::domain("beta_quantile requires p in [0, 1]"));
896 }
897 beta_shapes(alpha, beta)?;
898 if p == 0.0 {
899 return Ok(0.0);
900 }
901 if p == 1.0 {
902 return Ok(1.0);
903 }
904 invert_monotone(p, 0.0, 1.0, &mut |x| beta_cdf(x, alpha, beta), &mut |x| {
905 beta_pdf_raw(x, alpha, beta)
906 })
907}
908
909fn f_pdf_raw(x: f64, df1: f64, df2: f64) -> f64 {
914 let half1 = 0.5 * df1;
915 let half2 = 0.5 * df2;
916 exp(half1 * ln(df1) + half2 * ln(df2) + (half1 - 1.0) * ln(x)
917 - (half1 + half2) * ln(df2 + df1 * x)
918 - (lgamma(half1) + lgamma(half2) - lgamma(half1 + half2)))
919}
920
921fn f_degrees(df1: f64, df2: f64) -> Result<(), EngineError> {
922 if df1 <= 0.0 || df2 <= 0.0 || !df1.is_finite() || !df2.is_finite() {
923 return Err(EngineError::domain(
924 "F distribution requires finite positive degrees of freedom",
925 ));
926 }
927 Ok(())
928}
929
930pub fn f_pdf(x: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
931 if !x.is_finite() {
932 return Err(EngineError::domain("f_pdf requires a finite x"));
933 }
934 f_degrees(df1, df2)?;
935 if x < 0.0 {
936 return Ok(0.0);
937 }
938 if x == 0.0 {
939 return match df1.partial_cmp(&2.0) {
940 Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
941 "f_pdf is unbounded at x = 0 for df1 < 2",
942 )),
943 Some(std::cmp::Ordering::Equal) => Ok(1.0),
944 _ => Ok(0.0),
945 };
946 }
947 Ok(f_pdf_raw(x, df1, df2))
948}
949
950pub fn f_cdf(x: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
951 if !x.is_finite() {
952 return Err(EngineError::domain("f_cdf requires a finite x"));
953 }
954 f_degrees(df1, df2)?;
955 if x <= 0.0 {
956 return Ok(0.0);
957 }
958 let y = df1 * x / (df1 * x + df2);
959 ibeta(0.5 * df1, 0.5 * df2, y)
960}
961
962pub fn f_sf(x: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
965 if !x.is_finite() {
966 return Err(EngineError::domain("f_sf requires a finite x"));
967 }
968 f_degrees(df1, df2)?;
969 if x <= 0.0 {
970 return Ok(1.0);
971 }
972 let y = df2 / (df2 + df1 * x);
973 ibeta(0.5 * df2, 0.5 * df1, y)
974}
975
976pub fn f_quantile(p: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
977 if !(0.0..1.0).contains(&p) {
978 return Err(EngineError::domain("f_quantile requires p in [0, 1)"));
979 }
980 f_degrees(df1, df2)?;
981 if p == 0.0 {
982 return Ok(0.0);
983 }
984 let q = beta_quantile(p, 0.5 * df1, 0.5 * df2)?;
985 if q >= 1.0 {
986 return Err(EngineError::domain(
987 "f_quantile could not resolve the incomplete beta quantile",
988 ));
989 }
990 Ok(df2 * q / (df1 * (1.0 - q)))
991}
992
993fn negative_binomial_parameters(r: f64, p: f64) -> Result<(), EngineError> {
998 if !r.is_finite() || !p.is_finite() {
999 return Err(EngineError::domain(
1000 "negative binomial requires finite inputs",
1001 ));
1002 }
1003 if r <= 0.0 {
1004 return Err(EngineError::domain("negative binomial requires r > 0"));
1005 }
1006 if !(0.0..=1.0).contains(&p) {
1007 return Err(EngineError::domain(
1008 "negative binomial requires p in [0, 1]",
1009 ));
1010 }
1011 Ok(())
1012}
1013
1014pub fn negative_binomial_pmf(k: f64, r: f64, p: f64) -> Result<f64, EngineError> {
1015 if !k.is_finite() {
1016 return Err(EngineError::domain(
1017 "negative_binomial_pmf requires a finite k",
1018 ));
1019 }
1020 negative_binomial_parameters(r, p)?;
1021 if k < 0.0 {
1022 return Ok(0.0);
1023 }
1024 if p == 0.0 {
1025 return Ok(0.0);
1026 }
1027 if p == 1.0 {
1028 return Ok(if k == 0.0 { 1.0 } else { 0.0 });
1029 }
1030 if k == 0.0 {
1031 return Ok(exp(r * ln(p)));
1032 }
1033 let ln_pmf = lgamma(k + r) - lgamma(k + 1.0) - lgamma(r) + r * ln(p) + k * ln(1.0 - p);
1034 Ok(exp(ln_pmf))
1035}
1036
1037pub fn negative_binomial_cdf(k: f64, r: f64, p: f64) -> Result<f64, EngineError> {
1038 if !k.is_finite() {
1039 return Err(EngineError::domain(
1040 "negative_binomial_cdf requires a finite k",
1041 ));
1042 }
1043 negative_binomial_parameters(r, p)?;
1044 if k < 0.0 {
1045 return Ok(0.0);
1046 }
1047 if p == 0.0 {
1048 return Ok(0.0);
1049 }
1050 if p == 1.0 {
1051 return Ok(1.0);
1052 }
1053 ibeta(r, k + 1.0, p)
1054}
1055
1056pub fn invert_monotone(
1064 p: f64,
1065 mut lo: f64,
1066 mut hi: f64,
1067 cdf: &mut dyn FnMut(f64) -> Result<f64, EngineError>,
1068 pdf: &mut dyn FnMut(f64) -> f64,
1069) -> Result<f64, EngineError> {
1070 let mut expansions = 0u32;
1071 while cdf(lo)? > p {
1072 hi = lo;
1073 lo = if lo < 0.0 { lo * 2.0 } else { -1.0 };
1074 expansions += 1;
1075 if expansions > 4_000 || !lo.is_finite() {
1076 return Err(non_convergence("quantile bracket expansion"));
1077 }
1078 }
1079 while cdf(hi)? < p {
1080 lo = hi;
1081 hi = if hi > 0.0 { hi * 2.0 } else { 1.0 };
1082 expansions += 1;
1083 if expansions > 4_000 || !hi.is_finite() {
1084 return Err(non_convergence("quantile bracket expansion"));
1085 }
1086 }
1087 let mut x = 0.5 * (lo + hi);
1088 for _ in 0..400 {
1089 let mid = 0.5 * (lo + hi);
1090 x = mid;
1091 if cdf(mid)? < p {
1092 lo = mid;
1093 } else {
1094 hi = mid;
1095 }
1096 if (hi - lo) <= 1e-15 * (1.0 + abs(mid)) {
1097 break;
1098 }
1099 }
1100 for _ in 0..5 {
1101 let value = cdf(x)?;
1102 let density = pdf(x);
1103 if density <= 0.0 || !density.is_finite() {
1104 break;
1105 }
1106 let step = (value - p) / density;
1107 if !step.is_finite() {
1108 break;
1109 }
1110 let mut next = x - step;
1111 if !next.is_finite() {
1112 break;
1113 }
1114 if next < lo {
1115 next = 0.5 * (lo + x);
1116 } else if next > hi {
1117 next = 0.5 * (hi + x);
1118 }
1119 if abs(next - x) <= 1e-16 * (1.0 + abs(next)) {
1120 x = next;
1121 break;
1122 }
1123 x = next;
1124 }
1125 if !x.is_finite() {
1126 return Err(non_convergence("quantile inversion"));
1127 }
1128 Ok(x)
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133 use super::*;
1134
1135 fn close(actual: f64, expected: f64, tolerance: f64) {
1136 assert!(
1137 (actual - expected).abs() <= tolerance,
1138 "expected {expected}, got {actual} (tolerance {tolerance})"
1139 );
1140 }
1141
1142 #[test]
1143 fn normal_reference_values() {
1144 close(
1145 normal_cdf(1.96, 0.0, 1.0).unwrap(),
1146 0.975_002_104_851_779_5,
1147 1e-12,
1148 );
1149 close(
1150 normal_quantile(0.975, 0.0, 1.0).unwrap(),
1151 1.959_963_984_540_054,
1152 1e-9,
1153 );
1154 let tail = normal_sf(10.0, 0.0, 1.0).unwrap();
1157 close(tail / 7.619_853_024_160_593e-24, 1.0, 1e-9);
1158 close(normal_cdf(0.0, 0.0, 1.0).unwrap(), 0.5, 0.0);
1159 close(normal_quantile(0.5, 0.0, 1.0).unwrap(), 0.0, 0.0);
1160 }
1161
1162 #[test]
1163 fn student_t_reference_value() {
1164 close(
1172 student_t_cdf(2.0, 10.0).unwrap(),
1173 0.963_305_982_614_629_8,
1174 1e-12,
1175 );
1176 close(
1177 student_t_sf(2.0, 10.0).unwrap(),
1178 0.036_694_017_385_370_18,
1179 1e-12,
1180 );
1181 let q = student_t_quantile(0.975, 10.0).unwrap();
1182 close(q, 2.228_138_851_964_938_5, 1e-8);
1183 close(student_t_cdf(q, 10.0).unwrap(), 0.975, 1e-10);
1184 }
1185
1186 #[test]
1187 fn chi_square_reference_values() {
1188 close(
1189 chi_square_cdf(3.841_458_820_694_124, 1.0).unwrap(),
1190 0.95,
1191 1e-12,
1192 );
1193 let q = chi_square_quantile(0.95, 1.0).unwrap();
1194 close(q, 3.841_458_820_694_124, 1e-9);
1195 close(chi_square_cdf(q, 1.0).unwrap(), 0.95, 1e-10);
1196 close(chi_square_pdf(0.0, 2.0).unwrap(), 0.5, 0.0);
1197 }
1198
1199 #[test]
1200 fn binomial_reference_value() {
1201 close(binomial_cdf(5.0, 10.0, 0.5).unwrap(), 0.623_046_875, 1e-15);
1202 close(binomial_pmf(0.0, 0.0, 0.5).unwrap(), 1.0, 0.0);
1203 close(binomial_pmf(5.0, 10.0, 0.5).unwrap(), 252.0 / 1024.0, 1e-15);
1204 }
1205
1206 #[test]
1207 fn erfc_tail_is_relative_accurate() {
1208 close(erfc(5.0).unwrap(), 1.537_459_794_428_035_1e-12, 1e-24);
1210 close(erf(1.0).unwrap(), 0.842_700_792_949_715, 1e-15);
1211 }
1212
1213 #[test]
1214 fn binomial_edge_cases_are_exact() {
1215 close(binomial_pmf(0.0, 0.0, 0.5).unwrap(), 1.0, 0.0);
1216 close(
1217 binomial_pmf(0.0, 10.0, 0.3).unwrap(),
1218 0.7f64.powi(10),
1219 1e-18,
1220 );
1221 close(
1222 binomial_pmf(10.0, 10.0, 0.3).unwrap(),
1223 0.3f64.powi(10),
1224 1e-18,
1225 );
1226 close(binomial_cdf(-1.0, 10.0, 0.5).unwrap(), 0.0, 0.0);
1227 close(binomial_cdf(10.0, 10.0, 0.5).unwrap(), 1.0, 0.0);
1228 }
1229
1230 #[test]
1231 fn round_trips_are_tight() {
1232 for p in [1e-6, 0.01, 0.25, 0.5, 0.75, 0.99, 1.0 - 1e-6] {
1233 let q = normal_quantile(p, 0.0, 1.0).unwrap();
1234 close(normal_cdf(q, 0.0, 1.0).unwrap(), p, 1e-10);
1235 let t = student_t_quantile(p, 7.0).unwrap();
1236 close(student_t_cdf(t, 7.0).unwrap(), p, 1e-10);
1237 let c = chi_square_quantile(p, 3.0).unwrap();
1238 close(chi_square_cdf(c, 3.0).unwrap(), p, 1e-10);
1239 }
1240 }
1241}