scirs2_stats/traits.rs
1//! Statistical distribution traits
2//!
3//! This module defines the core traits for statistical distributions,
4//! including standard distributions and specialized circular distributions.
5
6use crate::error::StatsResult;
7use scirs2_core::ndarray::{Array, Array1, ArrayD, ArrayView1, IxDyn};
8use scirs2_core::numeric::Float;
9
10/// Base trait for all statistical distributions
11pub trait Distribution<F: Float> {
12 /// Mean (expected value) of the distribution
13 fn mean(&self) -> F;
14
15 /// Variance of the distribution
16 fn var(&self) -> F;
17
18 /// Standard deviation of the distribution
19 fn std(&self) -> F;
20
21 /// Generate random samples from the distribution
22 ///
23 /// # Arguments
24 ///
25 /// * `size` - Number of samples to generate
26 ///
27 /// # Returns
28 ///
29 /// An array of random samples from the distribution
30 fn rvs(&self, size: usize) -> StatsResult<Array1<F>>;
31
32 /// Generate random samples with a user-specified output shape.
33 ///
34 /// This is the SciPy-style ergonomic where `dist.rvs_array(&[8, 6])` returns
35 /// an `8 × 6` array of samples drawn IID from this distribution. The default
36 /// implementation draws `prod(shape)` scalar samples through `rvs(...)` and
37 /// reshapes them into a dynamically-dimensioned array.
38 ///
39 /// # Arguments
40 ///
41 /// * `shape` - Desired output shape. An empty shape produces a 0-D scalar array.
42 ///
43 /// # Returns
44 ///
45 /// A dynamic-rank array of IID samples shaped exactly as requested.
46 ///
47 /// # Errors
48 ///
49 /// Returns an error if the underlying `rvs` call fails or if the requested
50 /// shape's element count overflows `usize`.
51 fn rvs_array(&self, shape: &[usize]) -> StatsResult<ArrayD<F>> {
52 // Compute total element count, guarding against multiplication overflow.
53 let mut total: usize = 1;
54 for &dim in shape {
55 total = total.checked_mul(dim).ok_or_else(|| {
56 crate::error::StatsError::InvalidArgument(
57 "Requested rvs_array shape overflows usize".to_string(),
58 )
59 })?;
60 }
61
62 // Empty shape ⇒ 0-D array containing a single sample.
63 // Zero total ⇒ build an empty array with the requested shape (e.g. shape=[3, 0]).
64 if total == 0 {
65 return Array::from_shape_vec(IxDyn(shape), Vec::<F>::new()).map_err(|e| {
66 crate::error::StatsError::DimensionMismatch(format!(
67 "Failed to construct empty rvs_array with the requested shape: {e}"
68 ))
69 });
70 }
71
72 let flat = self.rvs(total)?;
73 Array::from_shape_vec(IxDyn(shape), flat.to_vec()).map_err(|e| {
74 crate::error::StatsError::DimensionMismatch(format!(
75 "Failed to reshape rvs samples into requested shape: {e}"
76 ))
77 })
78 }
79
80 /// Entropy of the distribution
81 fn entropy(&self) -> F;
82}
83
84/// Trait for continuous distributions
85pub trait ContinuousDistribution<F: Float>: Distribution<F> {
86 /// Probability density function (PDF)
87 ///
88 /// # Arguments
89 ///
90 /// * `x` - Point at which to evaluate the PDF
91 ///
92 /// # Returns
93 ///
94 /// The probability density at x
95 fn pdf(&self, x: F) -> F;
96
97 /// Cumulative distribution function (CDF)
98 ///
99 /// # Arguments
100 ///
101 /// * `x` - Point at which to evaluate the CDF
102 ///
103 /// # Returns
104 ///
105 /// The cumulative probability up to x
106 fn cdf(&self, x: F) -> F;
107
108 /// Percent point function (inverse CDF)
109 ///
110 /// # Arguments
111 ///
112 /// * `q` - Quantile (probability) in [0, 1]
113 ///
114 /// # Returns
115 ///
116 /// The value x such that CDF(x) = q
117 fn ppf(&self, q: F) -> StatsResult<F> {
118 // Default implementation using binary search
119 // This can be overridden for distributions with analytical ppf
120 if q < F::zero() || q > F::one() {
121 return Err(crate::error::StatsError::InvalidArgument(
122 "Quantile must be in [0, 1]".to_string(),
123 ));
124 }
125
126 // Use binary search to find the inverse
127 let mut low = F::from(-10.0).expect("Failed to convert constant to float");
128 let mut high = F::from(10.0).expect("Failed to convert constant to float");
129 let eps = F::from(1e-12).expect("Failed to convert constant to float");
130
131 // Find a reasonable search range
132 while self.cdf(low) > q {
133 low = low * F::from(2.0).expect("Failed to convert constant to float");
134 }
135 while self.cdf(high) < q {
136 high = high * F::from(2.0).expect("Failed to convert constant to float");
137 }
138
139 // Binary search
140 for _ in 0..100 {
141 let mid = (low + high) / F::from(2.0).expect("Failed to convert constant to float");
142 let cdf_mid = self.cdf(mid);
143
144 if (cdf_mid - q).abs() < eps {
145 return Ok(mid);
146 }
147
148 if cdf_mid < q {
149 low = mid;
150 } else {
151 high = mid;
152 }
153 }
154
155 Ok((low + high) / F::from(2.0).expect("Failed to convert constant to float"))
156 }
157
158 /// Vectorised PDF: evaluate `pdf` at every element of `x`.
159 ///
160 /// SciPy-style ergonomic — accepts a 1-D `ArrayView1` and returns an owned
161 /// `Array1` with the same length. The default implementation broadcasts
162 /// the scalar `pdf` via `mapv`. Distributions that can compute the PDF
163 /// faster on a batch are free to override this method.
164 ///
165 /// # Examples
166 ///
167 /// ```
168 /// use scirs2_core::ndarray::array;
169 /// use scirs2_stats::distributions::Normal;
170 /// use scirs2_stats::traits::ContinuousDistribution;
171 ///
172 /// let n = Normal::new(0.0_f64, 1.0).expect("normal");
173 /// let xs = array![-1.0, 0.0, 1.0];
174 /// let pdfs = n.pdf_array(&xs.view());
175 /// assert_eq!(pdfs.len(), 3);
176 /// ```
177 fn pdf_array(&self, x: &ArrayView1<F>) -> Array1<F> {
178 x.mapv(|xi| self.pdf(xi))
179 }
180
181 /// Vectorised CDF: evaluate `cdf` at every element of `x`.
182 ///
183 /// SciPy-style ergonomic. The default implementation broadcasts the scalar
184 /// `cdf` via `mapv`; distributions with a fast batch CDF should override.
185 fn cdf_array(&self, x: &ArrayView1<F>) -> Array1<F> {
186 x.mapv(|xi| self.cdf(xi))
187 }
188
189 /// Vectorised PPF: evaluate `ppf` at every element of `q`.
190 ///
191 /// Returns an error on the first element whose `ppf` call fails (typically
192 /// out-of-range quantiles). On success the output array has the same length
193 /// as the input view.
194 fn ppf_array(&self, q: &ArrayView1<F>) -> StatsResult<Array1<F>> {
195 let mut out = Array1::<F>::zeros(q.len());
196 for (i, &qi) in q.iter().enumerate() {
197 out[i] = self.ppf(qi)?;
198 }
199 Ok(out)
200 }
201}
202
203/// Trait for discrete distributions
204pub trait DiscreteDistribution<F: Float>: Distribution<F> {
205 /// Probability mass function (PMF)
206 ///
207 /// # Arguments
208 ///
209 /// * `k` - Point at which to evaluate the PMF
210 ///
211 /// # Returns
212 ///
213 /// The probability mass at k
214 fn pmf(&self, k: F) -> F;
215
216 /// Cumulative distribution function (CDF)
217 ///
218 /// # Arguments
219 ///
220 /// * `k` - Point at which to evaluate the CDF
221 ///
222 /// # Returns
223 ///
224 /// The cumulative probability up to k
225 fn cdf(&self, k: F) -> F;
226
227 /// Support of the distribution (range of possible values)
228 fn support(&self) -> (Option<F>, Option<F>) {
229 (None, None) // Default: unbounded support
230 }
231
232 /// Percent point function (inverse CDF)
233 ///
234 /// Returns the smallest integer `k` in the support such that `CDF(k) >= p`.
235 /// This default implementation uses exponential expansion + binary search on
236 /// the CDF, so it works for any discrete distribution that provides `cdf()`,
237 /// `mean()`, `std()`, and `support()`.
238 ///
239 /// Distributions with a closed-form quantile function should override this.
240 fn ppf(&self, p: F) -> StatsResult<F> {
241 // --- Validate input ---
242 if p < F::zero() || p > F::one() {
243 return Err(crate::error::StatsError::DomainError(
244 "Probability must be in [0, 1]".to_string(),
245 ));
246 }
247
248 // Determine support bounds (integer-valued).
249 let (sup_lo, sup_hi) = self.support();
250 let lo_bound: i64 = match sup_lo {
251 Some(v) => {
252 let raw = v.to_f64().unwrap_or(0.0);
253 raw.ceil() as i64
254 }
255 None => i64::MIN / 2,
256 };
257 let hi_bound: i64 = match sup_hi {
258 Some(v) => {
259 let raw = v.to_f64().unwrap_or(f64::MAX);
260 raw.floor() as i64
261 }
262 None => i64::MAX / 2,
263 };
264
265 // p == 0 → return support minimum.
266 if p <= F::zero() {
267 let lo_f = F::from(lo_bound.max(0)).ok_or_else(|| {
268 crate::error::StatsError::ComputationError(
269 "Cannot convert support lower bound to F".to_string(),
270 )
271 })?;
272 return Ok(lo_f);
273 }
274
275 // p == 1 → return support maximum (or +∞ for infinite support).
276 if p >= F::one() {
277 return match sup_hi {
278 Some(v) => Ok(v),
279 None => Ok(F::infinity()),
280 };
281 }
282
283 // --- Derive initial search range from mean ± 10·std ---
284 let mean_f64 = self.mean().to_f64().unwrap_or(0.0);
285 let std_f64 = self.std().to_f64().unwrap_or(1.0).max(1.0);
286
287 let mut lo: i64 = ((mean_f64 - 10.0 * std_f64).floor() as i64).max(lo_bound);
288 let mut hi: i64 = ((mean_f64 + 10.0 * std_f64).ceil() as i64).min(hi_bound);
289
290 // Clamp lo so it is at least support minimum.
291 lo = lo.max(lo_bound);
292 // Ensure hi > lo.
293 if hi <= lo {
294 hi = lo + 1;
295 }
296
297 // --- Expand lo leftward until CDF(lo) can be < p ---
298 // (For distributions starting at 0 this loop usually doesn't run.)
299 {
300 let mut step: i64 = (std_f64 * 10.0).ceil() as i64 + 1;
301 loop {
302 let lo_f = F::from(lo).ok_or_else(|| {
303 crate::error::StatsError::ComputationError(
304 "Overflow expanding lower bound in ppf".to_string(),
305 )
306 })?;
307 if self.cdf(lo_f) < p || lo <= lo_bound {
308 break;
309 }
310 lo = (lo - step).max(lo_bound);
311 step = step.saturating_mul(2);
312 }
313 lo = lo.max(lo_bound);
314 }
315
316 // --- Expand hi rightward until CDF(hi) >= p ---
317 {
318 let cap: i64 = 1_000_000_000i64.min(hi_bound);
319 let mut step: i64 = (std_f64 * 10.0).ceil() as i64 + 1;
320 let mut iters = 0usize;
321 loop {
322 let hi_f = F::from(hi).ok_or_else(|| {
323 crate::error::StatsError::ComputationError(
324 "Overflow expanding upper bound in ppf".to_string(),
325 )
326 })?;
327 if self.cdf(hi_f) >= p || hi >= cap {
328 break;
329 }
330 hi = (hi + step).min(cap);
331 step = step.saturating_mul(2);
332 iters += 1;
333 if iters > 100 {
334 break;
335 }
336 }
337 }
338
339 // --- Binary search: smallest integer k in [lo, hi] with CDF(k) >= p ---
340 // Invariant: CDF(hi) >= p (if not, hi is the best we have).
341 let mut left: i64 = lo;
342 let mut right: i64 = hi;
343
344 // Check if the right bound actually satisfies the condition.
345 let right_f = F::from(right).ok_or_else(|| {
346 crate::error::StatsError::ComputationError(
347 "Overflow converting hi bound in ppf".to_string(),
348 )
349 })?;
350 if self.cdf(right_f) < p {
351 // Support is exhausted; return hi.
352 return Ok(right_f);
353 }
354
355 while right - left > 1 {
356 let mid: i64 = left + (right - left) / 2;
357 let mid_f = F::from(mid).ok_or_else(|| {
358 crate::error::StatsError::ComputationError(
359 "Overflow converting mid in ppf binary search".to_string(),
360 )
361 })?;
362 if self.cdf(mid_f) >= p {
363 right = mid;
364 } else {
365 left = mid;
366 }
367 }
368
369 // After the loop, `right` satisfies cdf(right) >= p.
370 // Check if `left` also satisfies cdf(left) >= p — if so it is the
371 // *smaller* integer and is the correct answer.
372 let left_f = F::from(left).ok_or_else(|| {
373 crate::error::StatsError::ComputationError(
374 "Overflow converting left in ppf".to_string(),
375 )
376 })?;
377 if self.cdf(left_f) >= p {
378 return Ok(left_f);
379 }
380
381 F::from(right).ok_or_else(|| {
382 crate::error::StatsError::ComputationError(
383 "Overflow converting result in ppf".to_string(),
384 )
385 })
386 }
387
388 /// Log probability mass function
389 fn logpmf(&self, x: F) -> F {
390 self.pmf(x).ln()
391 }
392}
393
394/// Trait for circular distributions (distributions on the unit circle)
395pub trait CircularDistribution<F: Float>: Distribution<F> {
396 /// Probability density function for circular distributions
397 ///
398 /// # Arguments
399 ///
400 /// * `x` - Angle in radians
401 ///
402 /// # Returns
403 ///
404 /// The probability density at angle x
405 fn pdf(&self, x: F) -> F;
406
407 /// Cumulative distribution function for circular distributions
408 ///
409 /// # Arguments
410 ///
411 /// * `x` - Angle in radians
412 ///
413 /// # Returns
414 ///
415 /// The cumulative probability up to angle x
416 fn cdf(&self, x: F) -> F;
417
418 /// Generate a single random sample
419 ///
420 /// # Returns
421 ///
422 /// A single random sample from the distribution
423 fn rvs_single(&self) -> StatsResult<F>;
424
425 /// Circular mean (mean direction)
426 ///
427 /// # Returns
428 ///
429 /// The mean direction in radians
430 fn circular_mean(&self) -> F;
431
432 /// Circular variance
433 ///
434 /// # Returns
435 ///
436 /// The circular variance (1 - mean resultant length)
437 fn circular_variance(&self) -> F;
438
439 /// Circular standard deviation
440 ///
441 /// # Returns
442 ///
443 /// The circular standard deviation in radians
444 fn circular_std(&self) -> F;
445
446 /// Mean resultant length
447 ///
448 /// # Returns
449 ///
450 /// The mean resultant length (measure of concentration)
451 fn mean_resultant_length(&self) -> F;
452
453 /// Concentration parameter
454 ///
455 /// # Returns
456 ///
457 /// The concentration parameter of the distribution
458 fn concentration(&self) -> F;
459}
460
461/// Trait for multivariate distributions
462pub trait MultivariateDistribution<F: Float> {
463 /// Probability density function for multivariate distributions
464 ///
465 /// # Arguments
466 ///
467 /// * `x` - Point at which to evaluate the PDF
468 ///
469 /// # Returns
470 ///
471 /// The probability density at x
472 fn pdf(&self, x: &Array1<F>) -> F;
473
474 /// Generate random samples from the multivariate distribution
475 ///
476 /// # Arguments
477 ///
478 /// * `size` - Number of samples to generate
479 ///
480 /// # Returns
481 ///
482 /// A matrix where each row is a sample
483 fn rvs(&self, size: usize) -> StatsResult<scirs2_core::ndarray::Array2<F>>;
484
485 /// Mean vector of the distribution
486 fn mean(&self) -> Array1<F>;
487
488 /// Covariance matrix of the distribution
489 fn cov(&self) -> scirs2_core::ndarray::Array2<F>;
490
491 /// Dimensionality of the distribution
492 fn dim(&self) -> usize;
493
494 /// Log probability density function for multivariate distributions
495 fn logpdf(&self, x: &Array1<F>) -> F {
496 self.pdf(x).ln()
497 }
498
499 /// Generate a single random sample from the multivariate distribution
500 fn rvs_single(&self) -> StatsResult<Vec<F>> {
501 let samples = self.rvs(1)?;
502 Ok(samples.row(0).to_vec())
503 }
504}
505
506/// Trait for distributions that support fitting to data
507pub trait Fittable<F: Float> {
508 /// Fit the distribution to observed data
509 ///
510 /// # Arguments
511 ///
512 /// * `data` - Observed data points
513 ///
514 /// # Returns
515 ///
516 /// A fitted distribution instance
517 fn fit(data: &Array1<F>) -> StatsResult<Self>
518 where
519 Self: Sized;
520
521 /// Maximum likelihood estimation of parameters
522 ///
523 /// # Arguments
524 ///
525 /// * `data` - Observed data points
526 ///
527 /// # Returns
528 ///
529 /// A tuple of estimated parameters
530 fn mle(data: &Array1<F>) -> StatsResult<Vec<F>>;
531}
532
533/// Trait for distributions that can be truncated
534pub trait Truncatable<F: Float>: Distribution<F> {
535 /// Create a truncated version of the distribution
536 ///
537 /// # Arguments
538 ///
539 /// * `lower` - Lower bound (None for no lower bound)
540 /// * `upper` - Upper bound (None for no upper bound)
541 ///
542 /// # Returns
543 ///
544 /// A truncated version of the distribution
545 fn truncate(&self, lower: Option<F>, upper: Option<F>)
546 -> StatsResult<Box<dyn Distribution<F>>>;
547}
548
549/// Trait for continuous distributions that support CDF-related functions
550pub trait ContinuousCDF<F: Float>: ContinuousDistribution<F> {
551 /// Survival function (1 - CDF)
552 ///
553 /// # Arguments
554 ///
555 /// * `x` - Point at which to evaluate the survival function
556 ///
557 /// # Returns
558 ///
559 /// The survival probability at x (1 - CDF(x))
560 fn sf(&self, x: F) -> F {
561 F::one() - self.cdf(x)
562 }
563
564 /// Hazard function (PDF / (1 - CDF))
565 ///
566 /// # Arguments
567 ///
568 /// * `x` - Point at which to evaluate the hazard function
569 ///
570 /// # Returns
571 ///
572 /// The hazard rate at x
573 fn hazard(&self, x: F) -> F {
574 let sf_val = self.sf(x);
575 if sf_val == F::zero() {
576 F::infinity()
577 } else {
578 self.pdf(x) / sf_val
579 }
580 }
581
582 /// Cumulative hazard function (-ln(survival function))
583 ///
584 /// # Arguments
585 ///
586 /// * `x` - Point at which to evaluate the cumulative hazard function
587 ///
588 /// # Returns
589 ///
590 /// The cumulative hazard at x (-ln(1 - CDF(x)))
591 fn cumhazard(&self, x: F) -> F {
592 let sf_val = self.sf(x);
593 if sf_val <= F::zero() {
594 F::infinity()
595 } else {
596 -sf_val.ln()
597 }
598 }
599
600 /// Inverse survival function (PPF(1 - q))
601 ///
602 /// # Arguments
603 ///
604 /// * `q` - Probability in [0, 1]
605 ///
606 /// # Returns
607 ///
608 /// The value x such that SF(x) = q (equivalent to PPF(1 - q))
609 fn isf(&self, q: F) -> StatsResult<F> {
610 if q < F::zero() || q > F::one() {
611 return Err(crate::error::StatsError::InvalidArgument(
612 "Probability must be in [0, 1]".to_string(),
613 ));
614 }
615 self.ppf(F::one() - q)
616 }
617}
618
619/// Trait for discrete distributions that support CDF-related functions
620pub trait DiscreteCDF<F: Float>: DiscreteDistribution<F> {
621 /// Survival function (1 - CDF)
622 ///
623 /// # Arguments
624 ///
625 /// * `k` - Point at which to evaluate the survival function
626 ///
627 /// # Returns
628 ///
629 /// The survival probability at k (1 - CDF(k))
630 fn sf(&self, k: F) -> F {
631 F::one() - self.cdf(k)
632 }
633
634 /// Hazard function (PMF / (1 - CDF))
635 ///
636 /// # Arguments
637 ///
638 /// * `k` - Point at which to evaluate the hazard function
639 ///
640 /// # Returns
641 ///
642 /// The hazard rate at k
643 fn hazard(&self, k: F) -> F {
644 let sf_val = self.sf(k);
645 if sf_val == F::zero() {
646 F::infinity()
647 } else {
648 self.pmf(k) / sf_val
649 }
650 }
651
652 /// Cumulative hazard function (-ln(survival function))
653 ///
654 /// # Arguments
655 ///
656 /// * `k` - Point at which to evaluate the cumulative hazard function
657 ///
658 /// # Returns
659 ///
660 /// The cumulative hazard at k (-ln(1 - CDF(k)))
661 fn cumhazard(&self, k: F) -> F {
662 let sf_val = self.sf(k);
663 if sf_val <= F::zero() {
664 F::infinity()
665 } else {
666 -sf_val.ln()
667 }
668 }
669
670 /// Inverse survival function (PPF(1 - q))
671 ///
672 /// # Arguments
673 ///
674 /// * `q` - Probability in [0, 1]
675 ///
676 /// # Returns
677 ///
678 /// The value k such that SF(k) = q (equivalent to PPF(1 - q))
679 fn isf(&self, q: F) -> StatsResult<F> {
680 if q < F::zero() || q > F::one() {
681 return Err(crate::error::StatsError::InvalidArgument(
682 "Probability must be in [0, 1]".to_string(),
683 ));
684 }
685 self.ppf(F::one() - q)
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use crate::error::StatsResult;
693 use scirs2_core::ndarray::Array1;
694 use scirs2_core::numeric::Float;
695
696 // -----------------------------------------------------------------------
697 // Test fixture: Bernoulli(p) implemented via the default ppf
698 // -----------------------------------------------------------------------
699 struct TestBernoulli {
700 p: f64,
701 }
702
703 impl Distribution<f64> for TestBernoulli {
704 fn mean(&self) -> f64 {
705 self.p
706 }
707 fn var(&self) -> f64 {
708 self.p * (1.0 - self.p)
709 }
710 fn std(&self) -> f64 {
711 self.var().sqrt()
712 }
713 fn rvs(&self, size: usize) -> StatsResult<Array1<f64>> {
714 Ok(Array1::zeros(size))
715 }
716 fn entropy(&self) -> f64 {
717 0.0
718 }
719 }
720
721 impl DiscreteDistribution<f64> for TestBernoulli {
722 fn pmf(&self, k: f64) -> f64 {
723 if (k - 0.0).abs() < 1e-9 {
724 1.0 - self.p
725 } else if (k - 1.0).abs() < 1e-9 {
726 self.p
727 } else {
728 0.0
729 }
730 }
731 fn cdf(&self, k: f64) -> f64 {
732 if k < 0.0 {
733 0.0
734 } else if k < 1.0 {
735 1.0 - self.p
736 } else {
737 1.0
738 }
739 }
740 fn support(&self) -> (Option<f64>, Option<f64>) {
741 (Some(0.0), Some(1.0))
742 }
743 // ppf deliberately NOT overridden → exercises the default
744 }
745
746 // -----------------------------------------------------------------------
747 // Test fixture: Poisson-like (mu) via default ppf
748 // -----------------------------------------------------------------------
749 struct TestPoisson {
750 mu: f64,
751 }
752
753 impl Distribution<f64> for TestPoisson {
754 fn mean(&self) -> f64 {
755 self.mu
756 }
757 fn var(&self) -> f64 {
758 self.mu
759 }
760 fn std(&self) -> f64 {
761 self.mu.sqrt()
762 }
763 fn rvs(&self, size: usize) -> StatsResult<Array1<f64>> {
764 Ok(Array1::zeros(size))
765 }
766 fn entropy(&self) -> f64 {
767 0.0
768 }
769 }
770
771 impl DiscreteDistribution<f64> for TestPoisson {
772 fn pmf(&self, k: f64) -> f64 {
773 if k < 0.0 || (k - k.floor()).abs() > 1e-9 {
774 return 0.0;
775 }
776 let ki = k.round() as u64;
777 let mut log_pmf = -(self.mu) + (ki as f64) * self.mu.ln();
778 for i in 1..=ki {
779 log_pmf -= (i as f64).ln();
780 }
781 log_pmf.exp()
782 }
783 fn cdf(&self, k: f64) -> f64 {
784 if k < 0.0 {
785 return 0.0;
786 }
787 let ki = k.floor() as u64;
788 (0..=ki).map(|i| self.pmf(i as f64)).sum::<f64>()
789 }
790 fn support(&self) -> (Option<f64>, Option<f64>) {
791 (Some(0.0), None)
792 }
793 // ppf deliberately NOT overridden
794 }
795
796 // -----------------------------------------------------------------------
797 // Test fixture: Geometric(p) (# failures before first success, starting at 0)
798 // CDF(k) = 1 - (1-p)^(k+1)
799 // -----------------------------------------------------------------------
800 struct TestGeometric {
801 p: f64,
802 }
803
804 impl Distribution<f64> for TestGeometric {
805 fn mean(&self) -> f64 {
806 (1.0 - self.p) / self.p
807 }
808 fn var(&self) -> f64 {
809 (1.0 - self.p) / (self.p * self.p)
810 }
811 fn std(&self) -> f64 {
812 self.var().sqrt()
813 }
814 fn rvs(&self, size: usize) -> StatsResult<Array1<f64>> {
815 Ok(Array1::zeros(size))
816 }
817 fn entropy(&self) -> f64 {
818 0.0
819 }
820 }
821
822 impl DiscreteDistribution<f64> for TestGeometric {
823 fn pmf(&self, k: f64) -> f64 {
824 if k < 0.0 || (k - k.floor()).abs() > 1e-9 {
825 return 0.0;
826 }
827 let ki = k.round() as u64;
828 self.p * (1.0 - self.p).powi(ki as i32)
829 }
830 fn cdf(&self, k: f64) -> f64 {
831 if k < 0.0 {
832 return 0.0;
833 }
834 let ki = k.floor() as u64;
835 1.0 - (1.0 - self.p).powi(ki as i32 + 1)
836 }
837 fn support(&self) -> (Option<f64>, Option<f64>) {
838 (Some(0.0), None)
839 }
840 // ppf deliberately NOT overridden
841 }
842
843 // -----------------------------------------------------------------------
844 // Helper: verify cdf(ppf(p)) >= p (fundamental discrete PPF invariant)
845 // Also verifies ppf(p) is the SMALLEST such k.
846 // -----------------------------------------------------------------------
847 fn check_ppf_invariant<D: DiscreteDistribution<f64>>(dist: &D, p: f64) {
848 let k = dist
849 .ppf(p)
850 .unwrap_or_else(|e| panic!("ppf({p}) failed: {e}"));
851 let cdf_k = dist.cdf(k);
852 assert!(
853 cdf_k >= p - 1e-12,
854 "CDF({k}) = {cdf_k} < p = {p}: invariant cdf(ppf(p)) >= p violated"
855 );
856 // Verify that k-1 does NOT satisfy the condition (k is smallest).
857 if k >= 1.0 {
858 let cdf_km1 = dist.cdf(k - 1.0);
859 assert!(
860 cdf_km1 < p + 1e-12,
861 "CDF({}) = {} >= p = {p}: k={k} is not the SMALLEST such integer",
862 k - 1.0,
863 cdf_km1
864 );
865 }
866 }
867
868 // -----------------------------------------------------------------------
869 // Bernoulli(p=0.3) tests
870 // -----------------------------------------------------------------------
871 #[test]
872 fn test_default_ppf_bernoulli_invariants() {
873 let b = TestBernoulli { p: 0.3 };
874 for &p in &[0.1f64, 0.25, 0.5, 0.7, 0.9] {
875 check_ppf_invariant(&b, p);
876 }
877 }
878
879 #[test]
880 fn test_default_ppf_bernoulli_known_values() {
881 let b = TestBernoulli { p: 0.3 };
882 // CDF(0) = 0.7, so ppf(p) = 0 for p <= 0.7 and 1 for p > 0.7
883 assert_eq!(b.ppf(0.5).expect("ppf(0.5)"), 0.0);
884 assert_eq!(b.ppf(0.7).expect("ppf(0.7)"), 0.0);
885 assert_eq!(b.ppf(0.8).expect("ppf(0.8)"), 1.0);
886 assert_eq!(b.ppf(1.0).expect("ppf(1.0)"), 1.0);
887 }
888
889 #[test]
890 fn test_default_ppf_bernoulli_p0_returns_support_lo() {
891 let b = TestBernoulli { p: 0.7 };
892 // p == 0 → support minimum = 0
893 assert_eq!(b.ppf(0.0).expect("ppf(0.0)"), 0.0);
894 }
895
896 #[test]
897 fn test_default_ppf_bernoulli_p1_returns_support_hi() {
898 let b = TestBernoulli { p: 0.7 };
899 // p == 1 → support maximum = 1
900 assert_eq!(b.ppf(1.0).expect("ppf(1.0)"), 1.0);
901 }
902
903 #[test]
904 fn test_default_ppf_bernoulli_out_of_range() {
905 let b = TestBernoulli { p: 0.5 };
906 assert!(b.ppf(-0.1).is_err());
907 assert!(b.ppf(1.1).is_err());
908 }
909
910 // -----------------------------------------------------------------------
911 // Poisson(mu=3) tests
912 // -----------------------------------------------------------------------
913 #[test]
914 fn test_default_ppf_poisson_invariants() {
915 let po = TestPoisson { mu: 3.0 };
916 for &p in &[0.1f64, 0.25, 0.5, 0.75, 0.9] {
917 check_ppf_invariant(&po, p);
918 }
919 }
920
921 #[test]
922 fn test_default_ppf_poisson_median() {
923 let po = TestPoisson { mu: 3.0 };
924 // Median of Poisson(3) is 3
925 let med = po.ppf(0.5).expect("ppf(0.5)");
926 assert!(
927 (med - 3.0).abs() <= 1.0,
928 "Expected median near 3, got {med}"
929 );
930 }
931
932 #[test]
933 fn test_default_ppf_poisson_p0_returns_zero() {
934 let po = TestPoisson { mu: 5.0 };
935 assert_eq!(po.ppf(0.0).expect("ppf(0.0)"), 0.0);
936 }
937
938 #[test]
939 fn test_default_ppf_poisson_p1_returns_infinity() {
940 let po = TestPoisson { mu: 5.0 };
941 // Poisson has no finite upper bound → +∞
942 let v = po.ppf(1.0).expect("ppf(1.0)");
943 assert!(v.is_infinite() && v > 0.0, "Expected +∞, got {v}");
944 }
945
946 #[test]
947 fn test_default_ppf_poisson_large_mu_invariants() {
948 let po = TestPoisson { mu: 50.0 };
949 for &p in &[0.1f64, 0.5, 0.9] {
950 check_ppf_invariant(&po, p);
951 }
952 }
953
954 // -----------------------------------------------------------------------
955 // Geometric(p=0.4) tests
956 // -----------------------------------------------------------------------
957 #[test]
958 fn test_default_ppf_geometric_invariants() {
959 let g = TestGeometric { p: 0.4 };
960 for &p in &[0.1f64, 0.25, 0.5, 0.75, 0.9] {
961 check_ppf_invariant(&g, p);
962 }
963 }
964
965 #[test]
966 fn test_default_ppf_geometric_p0_returns_zero() {
967 let g = TestGeometric { p: 0.3 };
968 assert_eq!(g.ppf(0.0).expect("ppf(0.0)"), 0.0);
969 }
970
971 #[test]
972 fn test_default_ppf_geometric_p1_returns_infinity() {
973 let g = TestGeometric { p: 0.3 };
974 let v = g.ppf(1.0).expect("ppf(1.0)");
975 assert!(v.is_infinite() && v > 0.0, "Expected +∞, got {v}");
976 }
977
978 #[test]
979 fn test_default_ppf_geometric_heavy_tail() {
980 // Small p → heavy tail, exercises exponential expansion of hi
981 let g = TestGeometric { p: 0.05 };
982 for &p in &[0.5f64, 0.9, 0.99] {
983 check_ppf_invariant(&g, p);
984 }
985 }
986}