scirs2_stats/distributions/
inverse_gaussian.rs1use crate::distributions::normal::Normal;
70use crate::error::{StatsError, StatsResult};
71use crate::error_messages::validation;
72use crate::sampling::SampleableDistribution;
73use crate::traits::{ContinuousCDF, ContinuousDistribution, Distribution};
74use scirs2_core::ndarray::Array1;
75use scirs2_core::numeric::{Float, NumCast};
76use scirs2_core::random::prelude::*;
77use scirs2_core::random::rand_distributions::Distribution as _;
78use scirs2_core::random::{Normal as RandNormal, Uniform as RandUniform};
79
80pub struct InverseGaussian<F: Float> {
84 pub mu: F,
86 pub lambda: F,
88 normal_sampler: RandNormal<f64>,
90 uniform_sampler: RandUniform<f64>,
92 standard_normal: Normal<F>,
94}
95
96impl<F> InverseGaussian<F>
97where
98 F: Float + NumCast + std::fmt::Display,
99{
100 pub fn new(mu: F, lambda: F) -> StatsResult<Self> {
111 validation::ensure_positive(mu, "mu")?;
112 validation::ensure_positive(lambda, "lambda")?;
113
114 let normal_sampler = RandNormal::new(0.0_f64, 1.0_f64).map_err(|_| {
115 StatsError::ComputationError(
116 "Failed to construct standard-normal sampler for InverseGaussian".to_string(),
117 )
118 })?;
119 let uniform_sampler = RandUniform::new(0.0_f64, 1.0_f64).map_err(|_| {
120 StatsError::ComputationError(
121 "Failed to construct U(0,1) sampler for InverseGaussian".to_string(),
122 )
123 })?;
124
125 let standard_normal = Normal::new(F::zero(), F::one())?;
127
128 Ok(Self {
129 mu,
130 lambda,
131 normal_sampler,
132 uniform_sampler,
133 standard_normal,
134 })
135 }
136
137 pub fn pdf(&self, x: F) -> F {
141 if x <= F::zero() {
142 return F::zero();
143 }
144
145 let two = F::from(2.0).expect("2.0 representable");
146 let pi = F::from(std::f64::consts::PI).expect("π representable");
147
148 let prefactor = (self.lambda / (two * pi * x * x * x)).sqrt();
150 let diff = x - self.mu;
152 let exponent = -self.lambda * diff * diff / (two * self.mu * self.mu * x);
153
154 prefactor * exponent.exp()
155 }
156
157 pub fn cdf(&self, x: F) -> F {
167 if x <= F::zero() {
168 return F::zero();
169 }
170
171 let one = F::one();
172 let zero = F::zero();
173 let two = F::from(2.0).expect("2.0 representable");
174
175 let lam_over_x = self.lambda / x;
177 let u = lam_over_x.sqrt();
178 let x_over_mu = x / self.mu;
179 let z1 = u * (x_over_mu - one);
180 let z2 = -u * (x_over_mu + one);
181
182 let phi1 = self.standard_normal.cdf(z1);
183 let phi2 = self.standard_normal.cdf(z2);
184
185 let two_lambda_over_mu = two * self.lambda / self.mu;
186
187 let exp_term = two_lambda_over_mu.exp();
189 if exp_term.is_finite() {
190 let result = phi1 + exp_term * phi2;
191 if result < zero {
193 return zero;
194 }
195 if result > one {
196 return one;
197 }
198 return result;
199 }
200
201 if phi2 <= zero {
208 if phi1 < zero {
210 return zero;
211 }
212 if phi1 > one {
213 return one;
214 }
215 return phi1;
216 }
217
218 let log_second = phi2.ln() + two_lambda_over_mu;
219 let second = log_second.exp();
220 if second.is_finite() {
221 let result = phi1 + second;
222 if result < zero {
223 return zero;
224 }
225 if result > one {
226 return one;
227 }
228 return result;
229 }
230
231 one
233 }
234
235 pub fn ppf(&self, q: F) -> StatsResult<F> {
242 if q < F::zero() || q > F::one() {
243 return Err(StatsError::DomainError(
244 "Probability must be in [0, 1]".to_string(),
245 ));
246 }
247 if q == F::zero() {
248 return Ok(F::zero());
249 }
250 if q == F::one() {
251 return Ok(F::infinity());
252 }
253
254 let mu_f64: f64 = NumCast::from(self.mu).unwrap_or(1.0);
257 let lambda_f64: f64 = NumCast::from(self.lambda).unwrap_or(1.0);
258 let sigma_f64 = (mu_f64.powi(3) / lambda_f64).sqrt();
259
260 let low_f64 = mu_f64.min(1.0) * 1e-12_f64.max(f64::MIN_POSITIVE);
261 let mut lo = F::from(low_f64.max(f64::MIN_POSITIVE)).expect("F representable");
262 let mut hi_f64 = mu_f64 + 25.0 * sigma_f64;
263 if !hi_f64.is_finite() || hi_f64 <= mu_f64 {
264 hi_f64 = mu_f64.max(1.0) * 1e6;
265 }
266 let mut hi = F::from(hi_f64).expect("F representable");
267
268 let two = F::from(2.0).expect("2.0 representable");
270 for _ in 0..64 {
271 if self.cdf(hi) >= q {
272 break;
273 }
274 hi = hi * two;
275 if !<f64 as NumCast>::from(hi)
276 .map(f64::is_finite)
277 .unwrap_or(false)
278 {
279 return Ok(F::infinity());
280 }
281 }
282
283 let tol = F::from(1e-12).expect("tol representable");
285 for _ in 0..80 {
286 let mid = (lo + hi) / two;
287 if (hi - lo) < tol * (mid.abs() + F::one()) {
288 return Ok(mid);
289 }
290 let cdf_mid = self.cdf(mid);
291 if cdf_mid < q {
292 lo = mid;
293 } else {
294 hi = mid;
295 }
296 }
297
298 Ok((lo + hi) / two)
299 }
300
301 fn sample_one<R: Rng + ?Sized>(&self, rng: &mut R) -> StatsResult<F> {
310 let mu_f64: f64 = NumCast::from(self.mu)
311 .ok_or_else(|| StatsError::ComputationError("μ → f64 failed".to_string()))?;
312 let lambda_f64: f64 = NumCast::from(self.lambda)
313 .ok_or_else(|| StatsError::ComputationError("λ → f64 failed".to_string()))?;
314
315 let y: f64 = self.normal_sampler.sample(rng);
316 let v = y * y;
317 let mu_sq_v = mu_f64 * mu_f64 * v;
318 let coeff = mu_f64 / (2.0 * lambda_f64);
319 let radicand = 4.0 * mu_f64 * lambda_f64 * v + mu_sq_v * v;
320 let root = radicand.max(0.0).sqrt();
322 let x_small = mu_f64 + (mu_sq_v) / (2.0 * lambda_f64) - coeff * root;
323
324 let x_small = if x_small > 0.0 {
327 x_small
328 } else {
329 f64::MIN_POSITIVE
330 };
331
332 let u: f64 = self.uniform_sampler.sample(rng);
333 let chosen = if u <= mu_f64 / (mu_f64 + x_small) {
334 x_small
335 } else {
336 mu_f64 * mu_f64 / x_small
337 };
338
339 F::from(chosen).ok_or_else(|| {
340 StatsError::ComputationError("InverseGaussian sample → F failed".to_string())
341 })
342 }
343
344 pub fn rvs(&self, size: usize) -> StatsResult<Array1<F>> {
346 let mut rng = scirs2_core::random::thread_rng();
347 let mut out = Vec::with_capacity(size);
348 for _ in 0..size {
349 out.push(self.sample_one(&mut rng)?);
350 }
351 Ok(Array1::from(out))
352 }
353
354 pub fn mean_value(&self) -> F {
356 self.mu
357 }
358
359 pub fn variance(&self) -> F {
361 self.mu * self.mu * self.mu / self.lambda
362 }
363
364 pub fn skewness(&self) -> F {
366 let three = F::from(3.0).expect("3.0 representable");
367 three * (self.mu / self.lambda).sqrt()
368 }
369
370 pub fn kurtosis(&self) -> F {
372 let fifteen = F::from(15.0).expect("15.0 representable");
373 fifteen * self.mu / self.lambda
374 }
375}
376
377impl<F> Distribution<F> for InverseGaussian<F>
382where
383 F: Float + NumCast + std::fmt::Display,
384{
385 fn mean(&self) -> F {
386 self.mu
387 }
388
389 fn var(&self) -> F {
390 self.variance()
391 }
392
393 fn std(&self) -> F {
394 self.variance().sqrt()
395 }
396
397 fn rvs(&self, size: usize) -> StatsResult<Array1<F>> {
398 InverseGaussian::rvs(self, size)
399 }
400
401 fn entropy(&self) -> F {
402 let half = F::from(0.5).expect("0.5 representable");
413 let two = F::from(2.0).expect("2.0 representable");
414 let pi = F::from(std::f64::consts::PI).expect("π representable");
415 let e = F::from(std::f64::consts::E).expect("e representable");
416 half * (two * pi * e * self.mu * self.mu * self.mu / self.lambda).ln()
417 }
418}
419
420impl<F> ContinuousDistribution<F> for InverseGaussian<F>
421where
422 F: Float + NumCast + std::fmt::Display,
423{
424 fn pdf(&self, x: F) -> F {
425 InverseGaussian::pdf(self, x)
426 }
427
428 fn cdf(&self, x: F) -> F {
429 InverseGaussian::cdf(self, x)
430 }
431
432 fn ppf(&self, q: F) -> StatsResult<F> {
433 InverseGaussian::ppf(self, q)
434 }
435}
436
437impl<F> ContinuousCDF<F> for InverseGaussian<F> where F: Float + NumCast + std::fmt::Display {}
438
439impl<F> SampleableDistribution<F> for InverseGaussian<F>
440where
441 F: Float + NumCast + std::fmt::Display,
442{
443 fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
444 let arr = InverseGaussian::rvs(self, size)?;
445 Ok(arr.to_vec())
446 }
447}
448
449#[cfg(test)]
454mod tests {
455 use super::*;
456 use scirs2_core::ndarray::array;
457
458 fn trapezoid(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
460 let h = (b - a) / n as f64;
461 let mut sum = 0.5 * (f(a) + f(b));
462 for i in 1..n {
463 sum += f(a + h * i as f64);
464 }
465 sum * h
466 }
467
468 #[test]
469 fn test_issue_123_inverse_gaussian_construction_validation() {
470 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid params");
472 assert!((ig.mu - 1.0).abs() < 1e-12);
473 assert!((ig.lambda - 1.0).abs() < 1e-12);
474
475 assert!(InverseGaussian::<f64>::new(0.0, 1.0).is_err());
477 assert!(InverseGaussian::<f64>::new(-1.0, 1.0).is_err());
478
479 assert!(InverseGaussian::<f64>::new(1.0, 0.0).is_err());
481 assert!(InverseGaussian::<f64>::new(1.0, -2.0).is_err());
482 }
483
484 #[test]
485 fn test_issue_123_inverse_gaussian_pdf_outside_support() {
486 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
487 assert_eq!(ig.pdf(0.0), 0.0);
488 assert_eq!(ig.pdf(-1.0), 0.0);
489 assert_eq!(ig.pdf(-100.0), 0.0);
490 }
491
492 #[test]
493 fn test_issue_123_inverse_gaussian_pdf_known_value() {
494 let mu = 2.0_f64;
496 let lambda = 3.0_f64;
497 let ig = InverseGaussian::<f64>::new(mu, lambda).expect("valid");
498 let expected = (lambda / (2.0 * std::f64::consts::PI * mu.powi(3))).sqrt();
499 let got = ig.pdf(mu);
500 assert!(
501 (got - expected).abs() < 1e-12,
502 "pdf(μ) mismatch: got {got} expected {expected}"
503 );
504 }
505
506 #[test]
507 fn test_issue_123_inverse_gaussian_pdf_normalisation() {
508 let ig = InverseGaussian::<f64>::new(1.0, 2.0).expect("valid");
510 let integral = trapezoid(|x| ig.pdf(x), 1e-6, 50.0, 50_000);
511 assert!(
512 (integral - 1.0).abs() < 5e-3,
513 "PDF does not integrate to 1: got {integral}"
514 );
515 }
516
517 #[test]
518 fn test_issue_123_inverse_gaussian_pdf_matches_tweedie_special_case() {
519 use crate::distributions::tweedie::Tweedie;
522
523 let mu = 2.5_f64;
524 let lambda = 4.0_f64;
525 let ig = InverseGaussian::<f64>::new(mu, lambda).expect("valid IG");
526 let tw = Tweedie::<f64>::new(mu, 1.0 / lambda, 3.0).expect("valid Tweedie");
527 for &x in &[0.5_f64, 1.0, 2.0, 3.0, 5.0] {
528 let log_pdf_ig = ig.pdf(x).ln();
529 let log_pdf_tw = tw.log_pdf(x, 50);
530 assert!(
531 (log_pdf_ig - log_pdf_tw).abs() < 1e-10,
532 "log-pdf mismatch at x={x}: IG={log_pdf_ig} Tweedie={log_pdf_tw}"
533 );
534 }
535 }
536
537 #[test]
538 fn test_issue_123_inverse_gaussian_cdf_monotonicity() {
539 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
540 let xs: Vec<f64> = (1..=200).map(|i| 0.05 * i as f64).collect();
541 let mut prev = 0.0_f64;
542 for &x in &xs {
543 let c = ig.cdf(x);
544 assert!(
545 c >= prev - 1e-12,
546 "CDF not monotonic at x={x}: {c} < {prev}"
547 );
548 assert!((0.0..=1.0).contains(&c), "CDF out of [0, 1] at x={x}: {c}");
549 prev = c;
550 }
551 }
552
553 #[test]
554 fn test_issue_123_inverse_gaussian_cdf_limits() {
555 let ig = InverseGaussian::<f64>::new(2.0, 3.0).expect("valid");
556 assert_eq!(ig.cdf(0.0), 0.0);
558 assert!(ig.cdf(1e-12) < 1e-9, "CDF near 0 should be ≈ 0");
559 let far = 200.0;
561 assert!(ig.cdf(far) > 1.0 - 1e-9, "CDF far in tail should be ≈ 1");
562 }
563
564 #[test]
565 fn test_issue_123_inverse_gaussian_ppf_round_trip() {
566 let ig = InverseGaussian::<f64>::new(1.0, 1.5).expect("valid");
567 for &p in &[0.05_f64, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95] {
568 let x = ig.ppf(p).expect("ppf");
569 let p_back = ig.cdf(x);
570 assert!(
571 (p_back - p).abs() < 1e-6,
572 "round-trip failed at p={p}: ppf={x} cdf(ppf)={p_back}"
573 );
574 }
575 }
576
577 #[test]
578 fn test_issue_123_inverse_gaussian_ppf_out_of_range() {
579 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
580 assert!(ig.ppf(-0.1).is_err());
581 assert!(ig.ppf(1.1).is_err());
582 }
583
584 #[test]
585 fn test_issue_123_inverse_gaussian_moments() {
586 let mu = 3.0_f64;
587 let lambda = 5.0_f64;
588 let ig = InverseGaussian::<f64>::new(mu, lambda).expect("valid");
589 assert!((ig.mean_value() - mu).abs() < 1e-12);
590 assert!((ig.variance() - mu.powi(3) / lambda).abs() < 1e-12);
592 assert!((ig.skewness() - 3.0 * (mu / lambda).sqrt()).abs() < 1e-12);
594 assert!((ig.kurtosis() - 15.0 * mu / lambda).abs() < 1e-12);
596 }
597
598 #[test]
599 fn test_issue_123_inverse_gaussian_rvs_sample_size_and_positivity() {
600 let ig = InverseGaussian::<f64>::new(1.0, 2.0).expect("valid");
601 let samples = ig.rvs(2000).expect("rvs");
602 assert_eq!(samples.len(), 2000);
603 for &s in samples.iter() {
605 assert!(s.is_finite() && s > 0.0, "non-positive sample: {s}");
606 }
607 }
608
609 #[test]
610 fn test_issue_123_inverse_gaussian_mc_mean_variance() {
611 let mu = 2.0_f64;
615 let lambda = 3.0_f64;
616 let ig = InverseGaussian::<f64>::new(mu, lambda).expect("valid");
617 let n = 10_000_usize;
618 let samples = ig.rvs(n).expect("rvs");
619 let s = samples.as_slice().expect("contiguous");
620 let empirical_mean: f64 = s.iter().copied().sum::<f64>() / n as f64;
621 let empirical_var: f64 =
622 s.iter().map(|&v| (v - empirical_mean).powi(2)).sum::<f64>() / n as f64;
623 assert!(
624 (empirical_mean - mu).abs() < 0.10 * mu,
625 "MC mean {empirical_mean} far from {mu}"
626 );
627 let true_var = mu.powi(3) / lambda;
628 assert!(
629 (empirical_var - true_var).abs() < 0.20 * true_var,
630 "MC variance {empirical_var} far from {true_var}"
631 );
632 }
633
634 #[test]
637 fn test_issue_123_inverse_gaussian_pdf_array() {
638 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
639 let xs = array![0.5_f64, 1.0, 1.5, 2.0];
640 let pdfs = ig.pdf_array(&xs.view());
641 assert_eq!(pdfs.len(), 4);
642 for (i, &x) in xs.iter().enumerate() {
643 assert!(
644 (pdfs[i] - ig.pdf(x)).abs() < 1e-12,
645 "pdf_array[{i}] mismatch"
646 );
647 }
648 }
649
650 #[test]
651 fn test_issue_123_inverse_gaussian_cdf_array() {
652 let ig = InverseGaussian::<f64>::new(2.0, 3.0).expect("valid");
653 let xs = array![0.5_f64, 1.0, 2.0, 5.0];
654 let cdfs = ig.cdf_array(&xs.view());
655 for (i, &x) in xs.iter().enumerate() {
656 assert!(
657 (cdfs[i] - ig.cdf(x)).abs() < 1e-12,
658 "cdf_array[{i}] mismatch"
659 );
660 }
661 }
662
663 #[test]
664 fn test_issue_123_inverse_gaussian_ppf_array() {
665 let ig = InverseGaussian::<f64>::new(1.0, 2.0).expect("valid");
666 let qs = array![0.1_f64, 0.5, 0.9];
667 let xs = ig.ppf_array(&qs.view()).expect("ppf_array");
668 assert_eq!(xs.len(), 3);
669 for (i, &q) in qs.iter().enumerate() {
670 let scalar = ig.ppf(q).expect("ppf scalar");
671 assert!((xs[i] - scalar).abs() < 1e-12, "ppf_array[{i}] mismatch");
672 }
673 }
674
675 #[test]
678 fn test_issue_123_inverse_gaussian_rvs_array_shape() {
679 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
680 let arr = ig.rvs_array(&[8, 6]).expect("rvs_array");
681 assert_eq!(arr.shape(), &[8, 6]);
682 assert_eq!(arr.len(), 48);
683 for &v in arr.iter() {
684 assert!(v.is_finite() && v > 0.0);
685 }
686 }
687
688 #[test]
689 fn test_issue_123_inverse_gaussian_rvs_array_3d() {
690 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
691 let arr = ig.rvs_array(&[2, 3, 4]).expect("rvs_array 3d");
692 assert_eq!(arr.shape(), &[2, 3, 4]);
693 assert_eq!(arr.len(), 24);
694 }
695
696 #[test]
697 fn test_issue_123_inverse_gaussian_rvs_array_with_zero_dim() {
698 let ig = InverseGaussian::<f64>::new(1.0, 1.0).expect("valid");
700 let arr = ig.rvs_array(&[3, 0]).expect("rvs_array with zero dim");
701 assert_eq!(arr.shape(), &[3, 0]);
702 assert_eq!(arr.len(), 0);
703 }
704
705 #[test]
709 fn test_issue_123_normal_pdf_array_default_works() {
710 let n = Normal::<f64>::new(0.0, 1.0).expect("valid");
711 let xs = array![-1.0_f64, 0.0, 1.0];
712 let pdfs = n.pdf_array(&xs.view());
713 assert_eq!(pdfs.len(), 3);
714 assert!((pdfs[1] - 1.0_f64 / (2.0 * std::f64::consts::PI).sqrt()).abs() < 1e-7);
716 }
717
718 #[test]
719 fn test_issue_123_normal_rvs_array_default_works() {
720 let n = Normal::<f64>::new(0.0, 1.0).expect("valid");
721 let block = n.rvs_array(&[4, 5]).expect("rvs_array");
722 assert_eq!(block.shape(), &[4, 5]);
723 assert_eq!(block.len(), 20);
724 }
725}