1use u_numflow::special;
21use u_numflow::stats;
22
23pub fn ecdf(data: &[f64]) -> Option<(Vec<f64>, Vec<f64>)> {
44 if data.is_empty() || data.iter().any(|v| !v.is_finite()) {
45 return None;
46 }
47
48 let n = data.len() as f64;
49 let mut sorted: Vec<f64> = data.to_vec();
50 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
51
52 let mut values = Vec::new();
53 let mut probs = Vec::new();
54
55 let mut i = 0;
56 while i < sorted.len() {
57 let val = sorted[i];
58 let mut j = i;
60 while j < sorted.len() && (sorted[j] - val).abs() < 1e-300 {
61 j += 1;
62 }
63 values.push(val);
64 probs.push(j as f64 / n);
65 i = j;
66 }
67
68 Some((values, probs))
69}
70
71#[derive(Debug, Clone, Copy)]
77pub enum BinMethod {
78 Sturges,
80 Scott,
82 FreedmanDiaconis,
84 Fixed(usize),
86}
87
88#[derive(Debug, Clone)]
90pub struct HistogramBins {
91 pub n_bins: usize,
93 pub bin_width: f64,
95 pub edges: Vec<f64>,
97 pub counts: Vec<usize>,
99}
100
101pub fn histogram_bins(data: &[f64], method: BinMethod) -> Option<HistogramBins> {
119 let n = data.len();
120 if n < 2 || data.iter().any(|v| !v.is_finite()) {
121 return None;
122 }
123
124 let min_val = data.iter().cloned().reduce(f64::min)?;
125 let max_val = data.iter().cloned().reduce(f64::max)?;
126 let range = max_val - min_val;
127
128 if range < 1e-300 {
129 return None; }
131
132 let nf = n as f64;
133
134 let n_bins = match method {
135 BinMethod::Sturges => {
136 let k = (nf.log2()).ceil() as usize + 1;
137 k.max(2)
138 }
139 BinMethod::Scott => {
140 let sd = stats::std_dev(data)?;
141 if sd < 1e-300 {
142 return None;
143 }
144 let h = 3.49 * sd * nf.powf(-1.0 / 3.0);
145 ((range / h).ceil() as usize).max(2)
146 }
147 BinMethod::FreedmanDiaconis => {
148 let q1 = stats::quantile(data, 0.25)?;
149 let q3 = stats::quantile(data, 0.75)?;
150 let iqr = q3 - q1;
151 if iqr < 1e-300 {
152 let k = (nf.log2()).ceil() as usize + 1;
154 k.max(2)
155 } else {
156 let h = 2.0 * iqr * nf.powf(-1.0 / 3.0);
157 ((range / h).ceil() as usize).max(2)
158 }
159 }
160 BinMethod::Fixed(0) => return None,
163 BinMethod::Fixed(k) => k,
164 };
165
166 let bin_width = range / n_bins as f64;
167
168 let mut edges = Vec::with_capacity(n_bins + 1);
170 for i in 0..=n_bins {
171 edges.push(min_val + i as f64 * bin_width);
172 }
173
174 let mut counts = vec![0_usize; n_bins];
176 for &x in data {
177 let bin = ((x - min_val) / bin_width).floor() as usize;
178 let bin = bin.min(n_bins - 1); counts[bin] += 1;
180 }
181
182 Some(HistogramBins {
183 n_bins,
184 bin_width,
185 edges,
186 counts,
187 })
188}
189
190pub fn qq_plot_normal(data: &[f64]) -> Option<(Vec<f64>, Vec<f64>)> {
217 let n = data.len();
218 if n < 3 || data.iter().any(|v| !v.is_finite()) {
219 return None;
220 }
221
222 let mut sample: Vec<f64> = data.to_vec();
223 sample.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
224
225 let nf = n as f64;
226 let theoretical: Vec<f64> = (0..n)
227 .map(|i| {
228 let p = (i as f64 + 0.5) / nf;
229 special::inverse_normal_cdf(p)
230 })
231 .collect();
232
233 Some((theoretical, sample))
234}
235
236pub fn ks_test_normal(data: &[f64]) -> Option<(f64, f64)> {
262 let n = data.len();
263 if n < 5 || data.iter().any(|v| !v.is_finite()) {
264 return None;
265 }
266
267 let mean = stats::mean(data)?;
268 let sd = stats::std_dev(data)?;
269 if sd < 1e-300 {
270 return None;
271 }
272
273 let mut sorted: Vec<f64> = data.to_vec();
274 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
275
276 let nf = n as f64;
277 let mut d_stat = 0.0_f64;
278
279 for (i, &x) in sorted.iter().enumerate() {
280 let z = (x - mean) / sd;
281 let cdf = special::standard_normal_cdf(z);
282 let ecdf_above = (i + 1) as f64 / nf;
283 let ecdf_below = i as f64 / nf;
284 d_stat = d_stat.max((ecdf_above - cdf).abs());
285 d_stat = d_stat.max((ecdf_below - cdf).abs());
286 }
287
288 let lambda = (nf.sqrt() + 0.12 + 0.11 / nf.sqrt()) * d_stat;
291 let mut p_value = 0.0;
292 for k in 1..=100 {
293 let kf = k as f64;
294 let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
295 let term = sign * (-2.0 * kf * kf * lambda * lambda).exp();
296 p_value += term;
297 if term.abs() < 1e-15 {
298 break;
299 }
300 }
301 p_value = (2.0 * p_value).clamp(0.0, 1.0);
302
303 Some((d_stat, p_value))
304}
305
306#[derive(Debug, Clone, Copy)]
312pub enum BandwidthMethod {
313 Silverman,
319 Scott,
324 Manual(f64),
326}
327
328#[derive(Debug, Clone)]
330pub struct KdeResult {
331 pub x: Vec<f64>,
333 pub density: Vec<f64>,
335 pub bandwidth: f64,
337}
338
339pub fn kde(data: &[f64], method: BandwidthMethod, n_points: usize) -> Option<KdeResult> {
381 let n = data.len();
382 if n < 2 || n_points < 2 || data.iter().any(|v| !v.is_finite()) {
383 return None;
384 }
385
386 let bandwidth = kde_bandwidth(data, method)?;
387
388 let min_val = data.iter().cloned().reduce(f64::min)?;
390 let max_val = data.iter().cloned().reduce(f64::max)?;
391 let x_min = min_val - 3.0 * bandwidth;
392 let x_max = max_val + 3.0 * bandwidth;
393 let step = (x_max - x_min) / (n_points - 1) as f64;
394
395 let x: Vec<f64> = (0..n_points).map(|i| x_min + i as f64 * step).collect();
396
397 let inv_h = 1.0 / bandwidth;
399 let inv_nh = inv_h / n as f64;
400 let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
401
402 let density: Vec<f64> = x
403 .iter()
404 .map(|&xi| {
405 let sum: f64 = data
406 .iter()
407 .map(|&xj| {
408 let u = (xi - xj) * inv_h;
409 inv_sqrt_2pi * (-0.5 * u * u).exp()
410 })
411 .sum();
412 sum * inv_nh
413 })
414 .collect();
415
416 Some(KdeResult {
417 x,
418 density,
419 bandwidth,
420 })
421}
422
423pub fn kde_evaluate(data: &[f64], bandwidth: f64, x: f64) -> Option<f64> {
432 let n = data.len();
433 if n == 0 || bandwidth <= 0.0 || !bandwidth.is_finite() || !x.is_finite() {
434 return None;
435 }
436 if data.iter().any(|v| !v.is_finite()) {
437 return None;
438 }
439
440 let inv_h = 1.0 / bandwidth;
441 let inv_nh = inv_h / n as f64;
442 let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
443
444 let sum: f64 = data
445 .iter()
446 .map(|&xj| {
447 let u = (x - xj) * inv_h;
448 inv_sqrt_2pi * (-0.5 * u * u).exp()
449 })
450 .sum();
451
452 Some(sum * inv_nh)
453}
454
455pub fn kde_bandwidth(data: &[f64], method: BandwidthMethod) -> Option<f64> {
464 let n = data.len();
465 if n < 2 || data.iter().any(|v| !v.is_finite()) {
466 return None;
467 }
468
469 match method {
470 BandwidthMethod::Silverman => {
471 let sd = stats::std_dev(data)?;
472 if sd < 1e-300 {
473 return None;
474 }
475 let q1 = stats::quantile(data, 0.25)?;
476 let q3 = stats::quantile(data, 0.75)?;
477 let iqr = q3 - q1;
478 let spread = if iqr > 1e-300 { sd.min(iqr / 1.34) } else { sd };
479 Some(0.9 * spread * (n as f64).powf(-0.2))
480 }
481 BandwidthMethod::Scott => {
482 let sd = stats::std_dev(data)?;
483 if sd < 1e-300 {
484 return None;
485 }
486 Some(1.06 * sd * (n as f64).powf(-0.2))
487 }
488 BandwidthMethod::Manual(h) => {
489 if h <= 0.0 || !h.is_finite() {
490 None
491 } else {
492 Some(h)
493 }
494 }
495 }
496}
497
498#[derive(Debug, Clone)]
504pub struct FitResult {
505 pub distribution: String,
507 pub parameters: Vec<(String, f64)>,
509 pub log_likelihood: f64,
511 pub aic: f64,
513 pub bic: f64,
515 pub n_params: usize,
517}
518
519pub fn fit_normal(data: &[f64]) -> Option<FitResult> {
542 let n = data.len();
543 if n < 2 || data.iter().any(|v| !v.is_finite()) {
544 return None;
545 }
546
547 let nf = n as f64;
548 let mu = stats::mean(data)?;
549
550 let sum_sq: f64 = data.iter().map(|&x| (x - mu).powi(2)).sum();
552 let sigma_sq = sum_sq / nf;
553 if sigma_sq < 1e-300 {
554 return None;
555 }
556 let sigma = sigma_sq.sqrt();
557
558 let log_lik = -nf / 2.0 * (2.0 * std::f64::consts::PI).ln() - nf * sigma.ln() - nf / 2.0;
560
561 let k = 2;
562 let aic = -2.0 * log_lik + 2.0 * k as f64;
563 let bic = -2.0 * log_lik + k as f64 * nf.ln();
564
565 Some(FitResult {
566 distribution: "Normal".to_string(),
567 parameters: vec![("mu".to_string(), mu), ("sigma".to_string(), sigma)],
568 log_likelihood: log_lik,
569 aic,
570 bic,
571 n_params: k,
572 })
573}
574
575pub fn fit_exponential(data: &[f64]) -> Option<FitResult> {
596 let n = data.len();
597 if n < 2 || data.iter().any(|v| !v.is_finite()) {
598 return None;
599 }
600 if data.iter().any(|&v| v <= 0.0) {
602 return None;
603 }
604
605 let nf = n as f64;
606 let mean = stats::mean(data)?;
607 if mean < 1e-300 {
608 return None;
609 }
610
611 let lambda = 1.0 / mean;
612
613 let log_lik = nf * lambda.ln() - nf;
615
616 let k = 1;
617 let aic = -2.0 * log_lik + 2.0 * k as f64;
618 let bic = -2.0 * log_lik + k as f64 * nf.ln();
619
620 Some(FitResult {
621 distribution: "Exponential".to_string(),
622 parameters: vec![("lambda".to_string(), lambda)],
623 log_likelihood: log_lik,
624 aic,
625 bic,
626 n_params: k,
627 })
628}
629
630fn digamma(x: f64) -> f64 {
635 if x <= 0.0 {
636 return f64::NAN;
637 }
638
639 let mut val = 0.0;
641 let mut x = x;
642 while x < 8.0 {
643 val -= 1.0 / x;
644 x += 1.0;
645 }
646
647 let inv_x = 1.0 / x;
650 let inv_x2 = inv_x * inv_x;
651 val += x.ln()
652 - 0.5 * inv_x
653 - inv_x2 * (1.0 / 12.0 - inv_x2 * (1.0 / 120.0 - inv_x2 * (1.0 / 252.0)));
654
655 val
656}
657
658fn trigamma(x: f64) -> f64 {
662 if x <= 0.0 {
663 return f64::NAN;
664 }
665
666 let mut val = 0.0;
668 let mut x = x;
669 while x < 8.0 {
670 val += 1.0 / (x * x);
671 x += 1.0;
672 }
673
674 let inv_x = 1.0 / x;
677 let inv_x2 = inv_x * inv_x;
678 val += inv_x
679 + 0.5 * inv_x2
680 + inv_x2 * inv_x * (1.0 / 6.0 - inv_x2 * (1.0 / 30.0 - inv_x2 * (1.0 / 42.0)));
681
682 val
683}
684
685pub fn fit_gamma(data: &[f64]) -> Option<FitResult> {
714 let n = data.len();
715 if n < 2 || data.iter().any(|v| !v.is_finite()) {
716 return None;
717 }
718 if data.iter().any(|&v| v <= 0.0) {
720 return None;
721 }
722
723 let nf = n as f64;
724 let mean = stats::mean(data)?;
725 if mean < 1e-300 {
726 return None;
727 }
728
729 let var: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
731 if var < 1e-300 {
732 return None;
733 }
734
735 let sum_log: f64 = data.iter().map(|&x| x.ln()).sum();
736 let mean_log = sum_log / nf;
737 let target = mean.ln() - mean_log; let mut alpha = mean * mean / var;
741 if alpha < 1e-10 {
742 alpha = 0.5;
743 }
744
745 for _ in 0..200 {
747 let f = alpha.ln() - digamma(alpha) - target;
748 let f_prime = 1.0 / alpha - trigamma(alpha);
749 if f_prime.abs() < 1e-30 {
750 break;
751 }
752
753 let delta = f / f_prime;
754 alpha -= delta;
755
756 if alpha <= 0.0 {
758 alpha = 1e-10;
759 }
760
761 if delta.abs() < 1e-10 {
762 break;
763 }
764 }
765
766 if !alpha.is_finite() || alpha <= 0.0 {
767 return None;
768 }
769
770 let beta = alpha / mean; let sum_x: f64 = data.iter().sum();
774 let log_lik = nf * alpha * beta.ln() - nf * special::ln_gamma(alpha) + (alpha - 1.0) * sum_log
775 - beta * sum_x;
776
777 let k = 2;
778 let aic = -2.0 * log_lik + 2.0 * k as f64;
779 let bic = -2.0 * log_lik + k as f64 * nf.ln();
780
781 Some(FitResult {
782 distribution: "Gamma".to_string(),
783 parameters: vec![("alpha".to_string(), alpha), ("beta".to_string(), beta)],
784 log_likelihood: log_lik,
785 aic,
786 bic,
787 n_params: k,
788 })
789}
790
791pub fn fit_lognormal(data: &[f64]) -> Option<FitResult> {
815 let n = data.len();
816 if n < 2 || data.iter().any(|v| !v.is_finite()) {
817 return None;
818 }
819 if data.iter().any(|&v| v <= 0.0) {
820 return None;
821 }
822
823 let nf = n as f64;
824 let log_data: Vec<f64> = data.iter().map(|&x| x.ln()).collect();
825
826 let mu = log_data.iter().sum::<f64>() / nf;
827 let sigma_sq = log_data.iter().map(|&y| (y - mu).powi(2)).sum::<f64>() / nf;
828 if sigma_sq < 1e-300 {
829 return None;
830 }
831 let sigma = sigma_sq.sqrt();
832
833 let sum_log: f64 = log_data.iter().sum();
835 let log_lik =
836 -nf / 2.0 * (2.0 * std::f64::consts::PI).ln() - nf * sigma.ln() - nf / 2.0 - sum_log;
837
838 let k = 2;
839 let aic = -2.0 * log_lik + 2.0 * k as f64;
840 let bic = -2.0 * log_lik + k as f64 * nf.ln();
841
842 Some(FitResult {
843 distribution: "LogNormal".to_string(),
844 parameters: vec![("mu".to_string(), mu), ("sigma".to_string(), sigma)],
845 log_likelihood: log_lik,
846 aic,
847 bic,
848 n_params: k,
849 })
850}
851
852pub fn fit_poisson(data: &[f64]) -> Option<FitResult> {
876 let n = data.len();
877 if n < 2 || data.iter().any(|v| !v.is_finite()) {
878 return None;
879 }
880 if data.iter().any(|&v| v < 0.0 || v.fract() != 0.0) {
881 return None;
882 }
883
884 let nf = n as f64;
885 let lambda = stats::mean(data)?;
886 if lambda < 1e-300 {
887 return None; }
889
890 let sum_log_fact: f64 = data.iter().map(|&x| special::ln_gamma(x + 1.0)).sum();
893 let log_lik = nf * lambda * lambda.ln() - nf * lambda - sum_log_fact;
894
895 let k = 1;
896 let aic = -2.0 * log_lik + 2.0 * k as f64;
897 let bic = -2.0 * log_lik + k as f64 * nf.ln();
898
899 Some(FitResult {
900 distribution: "Poisson".to_string(),
901 parameters: vec![("lambda".to_string(), lambda)],
902 log_likelihood: log_lik,
903 aic,
904 bic,
905 n_params: k,
906 })
907}
908
909pub fn fit_beta(data: &[f64]) -> Option<FitResult> {
939 let n = data.len();
940 if n < 2 || data.iter().any(|v| !v.is_finite()) {
941 return None;
942 }
943 if data.iter().any(|&v| v <= 0.0 || v >= 1.0) {
945 return None;
946 }
947
948 let nf = n as f64;
949 let mean = stats::mean(data)?;
950 let var = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
951 if var < 1e-300 {
952 return None;
953 }
954
955 let sum_log_x: f64 = data.iter().map(|&x| x.ln()).sum();
956 let sum_log_1mx: f64 = data.iter().map(|&x| (1.0 - x).ln()).sum();
957 let mean_log_x = sum_log_x / nf;
958 let mean_log_1mx = sum_log_1mx / nf;
959
960 let factor = mean * (1.0 - mean) / var - 1.0;
962 if factor <= 0.0 {
963 return None;
964 }
965 let mut alpha = mean * factor;
966 let mut beta = (1.0 - mean) * factor;
967
968 if alpha < 0.01 {
969 alpha = 0.5;
970 }
971 if beta < 0.01 {
972 beta = 0.5;
973 }
974
975 for _ in 0..200 {
977 let psi_ab = digamma(alpha + beta);
978 let tri_ab = trigamma(alpha + beta);
979
980 let f1 = digamma(alpha) - psi_ab - mean_log_x;
981 let f2 = digamma(beta) - psi_ab - mean_log_1mx;
982
983 let j11 = trigamma(alpha) - tri_ab;
984 let j12 = -tri_ab;
985 let j21 = -tri_ab;
986 let j22 = trigamma(beta) - tri_ab;
987
988 let det = j11 * j22 - j12 * j21;
989 if det.abs() < 1e-30 {
990 break;
991 }
992
993 let da = (j22 * f1 - j12 * f2) / det;
994 let db = (j11 * f2 - j21 * f1) / det;
995
996 alpha -= da;
997 beta -= db;
998
999 if alpha <= 0.0 {
1000 alpha = 1e-10;
1001 }
1002 if beta <= 0.0 {
1003 beta = 1e-10;
1004 }
1005
1006 if da.abs() < 1e-10 && db.abs() < 1e-10 {
1007 break;
1008 }
1009 }
1010
1011 if !alpha.is_finite() || !beta.is_finite() || alpha <= 0.0 || beta <= 0.0 {
1012 return None;
1013 }
1014
1015 let log_lik = nf
1018 * (special::ln_gamma(alpha + beta) - special::ln_gamma(alpha) - special::ln_gamma(beta))
1019 + (alpha - 1.0) * sum_log_x
1020 + (beta - 1.0) * sum_log_1mx;
1021
1022 let k = 2;
1023 let aic = -2.0 * log_lik + 2.0 * k as f64;
1024 let bic = -2.0 * log_lik + k as f64 * nf.ln();
1025
1026 Some(FitResult {
1027 distribution: "Beta".to_string(),
1028 parameters: vec![("alpha".to_string(), alpha), ("beta".to_string(), beta)],
1029 log_likelihood: log_lik,
1030 aic,
1031 bic,
1032 n_params: k,
1033 })
1034}
1035
1036pub fn fit_weibull(data: &[f64]) -> Option<FitResult> {
1056 if data.len() < 2 || data.iter().any(|&v| !(v.is_finite() && v > 0.0)) {
1057 return None;
1058 }
1059 let r = crate::weibull::weibull_mle(data)?;
1060 let nf = data.len() as f64;
1061 let k = 2;
1062 Some(FitResult {
1063 distribution: "Weibull".to_string(),
1064 parameters: vec![
1065 ("shape".to_string(), r.shape),
1066 ("scale".to_string(), r.scale),
1067 ],
1068 log_likelihood: r.log_likelihood,
1069 aic: -2.0 * r.log_likelihood + 2.0 * k as f64,
1070 bic: -2.0 * r.log_likelihood + k as f64 * nf.ln(),
1071 n_params: k,
1072 })
1073}
1074
1075pub fn fit_best(data: &[f64]) -> Vec<FitResult> {
1101 let mut results = Vec::new();
1102
1103 if let Some(r) = fit_normal(data) {
1104 results.push(r);
1105 }
1106 if let Some(r) = fit_exponential(data) {
1107 results.push(r);
1108 }
1109 if let Some(r) = fit_gamma(data) {
1110 results.push(r);
1111 }
1112 if let Some(r) = fit_lognormal(data) {
1113 results.push(r);
1114 }
1115 if let Some(r) = fit_weibull(data) {
1116 results.push(r);
1117 }
1118 if let Some(r) = fit_beta(data) {
1119 results.push(r);
1120 }
1121
1122 results.sort_by(|a, b| {
1123 a.aic
1124 .partial_cmp(&b.aic)
1125 .unwrap_or(std::cmp::Ordering::Equal)
1126 });
1127 results
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133
1134 #[test]
1139 fn ecdf_basic() {
1140 let (vals, probs) = ecdf(&[3.0, 1.0, 2.0]).expect("should compute");
1141 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1142 assert!((probs[0] - 1.0 / 3.0).abs() < 1e-10);
1143 assert!((probs[1] - 2.0 / 3.0).abs() < 1e-10);
1144 assert!((probs[2] - 1.0).abs() < 1e-10);
1145 }
1146
1147 #[test]
1148 fn ecdf_with_ties() {
1149 let (vals, probs) = ecdf(&[1.0, 2.0, 2.0, 3.0]).expect("should compute");
1150 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1151 assert!((probs[0] - 0.25).abs() < 1e-10);
1152 assert!((probs[1] - 0.75).abs() < 1e-10); assert!((probs[2] - 1.0).abs() < 1e-10);
1154 }
1155
1156 #[test]
1157 fn ecdf_single() {
1158 let (vals, probs) = ecdf(&[5.0]).expect("should compute");
1159 assert_eq!(vals, vec![5.0]);
1160 assert!((probs[0] - 1.0).abs() < 1e-10);
1161 }
1162
1163 #[test]
1164 fn ecdf_empty() {
1165 assert!(ecdf(&[]).is_none());
1166 }
1167
1168 #[test]
1169 fn ecdf_nan() {
1170 assert!(ecdf(&[1.0, f64::NAN, 3.0]).is_none());
1171 }
1172
1173 #[test]
1178 fn hist_sturges_basic() {
1179 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1180 let r = histogram_bins(&data, BinMethod::Sturges).expect("should compute");
1181 assert!(r.n_bins >= 5);
1182 assert_eq!(r.edges.len(), r.n_bins + 1);
1183 assert_eq!(r.counts.len(), r.n_bins);
1184 let total: usize = r.counts.iter().sum();
1185 assert_eq!(total, 100);
1186 }
1187
1188 #[test]
1189 fn hist_scott() {
1190 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1191 let r = histogram_bins(&data, BinMethod::Scott).expect("should compute");
1192 assert!(r.n_bins >= 3);
1193 let total: usize = r.counts.iter().sum();
1194 assert_eq!(total, 100);
1195 }
1196
1197 #[test]
1198 fn hist_fd() {
1199 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1200 let r = histogram_bins(&data, BinMethod::FreedmanDiaconis).expect("should compute");
1201 assert!(r.n_bins >= 3);
1202 let total: usize = r.counts.iter().sum();
1203 assert_eq!(total, 100);
1204 }
1205
1206 #[test]
1207 fn hist_edge_cases() {
1208 assert!(histogram_bins(&[1.0], BinMethod::Sturges).is_none()); assert!(histogram_bins(&[5.0, 5.0, 5.0], BinMethod::Sturges).is_none());
1210 }
1212
1213 #[test]
1214 fn hist_fixed_exact_count() {
1215 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1216 let r = histogram_bins(&data, BinMethod::Fixed(5)).expect("should compute");
1217 assert_eq!(r.n_bins, 5);
1218 assert_eq!(r.edges.len(), 6);
1219 assert_eq!(r.counts.len(), 5);
1220 let total: usize = r.counts.iter().sum();
1221 assert_eq!(total, 100);
1222 }
1223
1224 #[test]
1225 fn hist_fixed_single_bin() {
1226 let data = [1.0, 2.0, 3.0];
1227 let r = histogram_bins(&data, BinMethod::Fixed(1)).expect("should compute");
1228 assert_eq!(r.n_bins, 1);
1229 assert_eq!(r.counts, vec![3]);
1230 }
1231
1232 #[test]
1233 fn hist_fixed_zero_is_invalid() {
1234 let data = [1.0, 2.0, 3.0];
1235 assert!(histogram_bins(&data, BinMethod::Fixed(0)).is_none());
1236 }
1237
1238 #[test]
1239 fn hist_fixed_shares_input_guards() {
1240 assert!(histogram_bins(&[1.0], BinMethod::Fixed(5)).is_none()); assert!(histogram_bins(&[5.0, 5.0, 5.0], BinMethod::Fixed(5)).is_none());
1242 }
1244
1245 #[test]
1250 fn qq_basic() {
1251 let data = [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5];
1252 let (theo, samp) = qq_plot_normal(&data).expect("should compute");
1253 assert_eq!(theo.len(), 7);
1254 assert_eq!(samp.len(), 7);
1255 assert!((samp[0] + 1.5).abs() < 1e-10);
1257 assert!((samp[6] - 1.5).abs() < 1e-10);
1258 assert!(theo[3].abs() < 0.1);
1260 }
1261
1262 #[test]
1263 fn qq_edge_cases() {
1264 assert!(qq_plot_normal(&[1.0, 2.0]).is_none()); }
1266
1267 #[test]
1272 fn ks_normal_data() {
1273 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5];
1275 let (d, p) = ks_test_normal(&data).expect("should compute");
1276 assert!(d > 0.0 && d < 1.0, "D = {d}");
1277 assert!(p > 0.05, "p = {p}");
1278 }
1279
1280 #[test]
1281 fn ks_non_normal_data() {
1282 let data: Vec<f64> = (0..50).map(|i| i as f64 * 2.0).collect();
1284 let (d, _p) = ks_test_normal(&data).expect("should compute");
1285 assert!(d > 0.0);
1286 }
1287
1288 #[test]
1289 fn ks_edge_cases() {
1290 assert!(ks_test_normal(&[1.0, 2.0, 3.0, 4.0]).is_none()); assert!(ks_test_normal(&[5.0, 5.0, 5.0, 5.0, 5.0]).is_none()); }
1293
1294 #[test]
1299 fn kde_silverman_basic() {
1300 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1301 let r = kde(&data, BandwidthMethod::Silverman, 256).expect("should compute");
1302 assert_eq!(r.x.len(), 256);
1303 assert_eq!(r.density.len(), 256);
1304 assert!(r.bandwidth > 0.0);
1305 assert!(r.density.iter().all(|&d| d >= 0.0));
1307 let dx = r.x[1] - r.x[0];
1309 let integral: f64 = r.density.iter().sum::<f64>() * dx;
1310 assert!(
1311 (integral - 1.0).abs() < 0.05,
1312 "integral = {integral}, expected ≈ 1.0"
1313 );
1314 }
1315
1316 #[test]
1317 fn kde_scott_basic() {
1318 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1319 let r = kde(&data, BandwidthMethod::Scott, 256).expect("should compute");
1320 assert!(r.bandwidth > 0.0);
1321 let dx = r.x[1] - r.x[0];
1322 let integral: f64 = r.density.iter().sum::<f64>() * dx;
1323 assert!(
1324 (integral - 1.0).abs() < 0.05,
1325 "integral = {integral}, expected ≈ 1.0"
1326 );
1327 }
1328
1329 #[test]
1330 fn kde_manual_bandwidth() {
1331 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1332 let r = kde(&data, BandwidthMethod::Manual(0.5), 128).expect("should compute");
1333 assert!((r.bandwidth - 0.5).abs() < 1e-10);
1334 assert_eq!(r.x.len(), 128);
1335 }
1336
1337 #[test]
1338 fn kde_edge_cases() {
1339 assert!(kde(&[1.0], BandwidthMethod::Silverman, 256).is_none());
1341 assert!(kde(&[], BandwidthMethod::Silverman, 256).is_none());
1343 assert!(kde(&[1.0, f64::NAN], BandwidthMethod::Silverman, 256).is_none());
1345 assert!(kde(&[5.0, 5.0, 5.0], BandwidthMethod::Silverman, 256).is_none());
1347 assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(1.0), 1).is_none());
1349 assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(0.0), 256).is_none());
1351 assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(-1.0), 256).is_none());
1352 }
1353
1354 #[test]
1355 fn kde_bimodal() {
1356 let mut data = Vec::new();
1358 for i in 0..50 {
1359 data.push(i as f64 * 0.1); }
1361 for i in 0..50 {
1362 data.push(10.0 + i as f64 * 0.1); }
1364 let r = kde(&data, BandwidthMethod::Silverman, 512).expect("should compute");
1365 let idx_cluster = r.x.iter().position(|&x| x >= 2.5).expect("grid point");
1368 let idx_valley = r.x.iter().position(|&x| x >= 7.5).expect("grid point");
1369 assert!(
1370 r.density[idx_cluster] > r.density[idx_valley],
1371 "cluster density ({}) should exceed valley density ({})",
1372 r.density[idx_cluster],
1373 r.density[idx_valley]
1374 );
1375 }
1376
1377 #[test]
1378 fn kde_evaluate_single() {
1379 let data = [0.0, 1.0, 2.0, 3.0, 4.0];
1380 let h = 1.0;
1381 let d = kde_evaluate(&data, h, 2.0).expect("should compute");
1382 assert!(d > 0.0);
1383 let d_edge = kde_evaluate(&data, h, -5.0).expect("should compute");
1385 assert!(d > d_edge, "center density ({d}) > edge density ({d_edge})");
1386 }
1387
1388 #[test]
1389 fn kde_evaluate_edge_cases() {
1390 assert!(kde_evaluate(&[], 1.0, 0.0).is_none());
1391 assert!(kde_evaluate(&[1.0], 0.0, 0.0).is_none());
1392 assert!(kde_evaluate(&[1.0], -1.0, 0.0).is_none());
1393 assert!(kde_evaluate(&[1.0], 1.0, f64::NAN).is_none());
1394 assert!(kde_evaluate(&[f64::NAN], 1.0, 0.0).is_none());
1395 }
1396
1397 #[test]
1398 fn kde_bandwidth_methods() {
1399 let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
1400 let h_silv = kde_bandwidth(&data, BandwidthMethod::Silverman).expect("silverman");
1401 let h_scott = kde_bandwidth(&data, BandwidthMethod::Scott).expect("scott");
1402 let h_manual = kde_bandwidth(&data, BandwidthMethod::Manual(2.5)).expect("manual");
1403 assert!(h_silv > 0.0);
1404 assert!(h_scott > 0.0);
1405 assert!((h_manual - 2.5).abs() < 1e-10);
1406 assert!(h_scott > h_silv);
1409 }
1410
1411 #[test]
1412 fn kde_bandwidth_edge_cases() {
1413 assert!(kde_bandwidth(&[1.0], BandwidthMethod::Silverman).is_none());
1414 assert!(kde_bandwidth(&[5.0, 5.0], BandwidthMethod::Silverman).is_none());
1415 assert!(kde_bandwidth(&[1.0, 2.0], BandwidthMethod::Manual(0.0)).is_none());
1416 }
1417
1418 #[test]
1419 fn kde_grid_extends_beyond_data() {
1420 let data = [10.0, 20.0, 30.0];
1421 let r = kde(&data, BandwidthMethod::Manual(2.0), 64).expect("should compute");
1422 assert!(r.x[0] < 10.0 - 2.0, "grid starts at {}", r.x[0]);
1424 assert!(
1425 *r.x.last().expect("non-empty") > 30.0 + 2.0,
1426 "grid ends at {}",
1427 r.x.last().expect("non-empty")
1428 );
1429 }
1430
1431 #[test]
1436 fn fit_normal_basic() {
1437 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1438 let r = fit_normal(&data).expect("should compute");
1439 assert_eq!(r.distribution, "Normal");
1440 assert_eq!(r.n_params, 2);
1441 assert!((r.parameters[0].1 - 5.0).abs() < 1e-10, "μ̂ = 5.0");
1442 let expected_sigma = (32.0_f64 / 8.0).sqrt();
1445 assert!(
1446 (r.parameters[1].1 - expected_sigma).abs() < 1e-10,
1447 "σ̂ = {}, expected {}",
1448 r.parameters[1].1,
1449 expected_sigma
1450 );
1451 assert!(r.log_likelihood.is_finite());
1452 assert!(r.aic.is_finite());
1453 assert!(r.bic.is_finite());
1454 }
1455
1456 #[test]
1457 fn fit_normal_edge_cases() {
1458 assert!(fit_normal(&[1.0]).is_none()); assert!(fit_normal(&[]).is_none());
1460 assert!(fit_normal(&[1.0, f64::NAN]).is_none());
1461 assert!(fit_normal(&[5.0, 5.0, 5.0]).is_none()); }
1463
1464 #[test]
1465 fn fit_exponential_basic() {
1466 let data = [0.5, 1.2, 0.8, 2.1, 0.3, 1.5, 0.9, 1.8];
1467 let r = fit_exponential(&data).expect("should compute");
1468 assert_eq!(r.distribution, "Exponential");
1469 assert_eq!(r.n_params, 1);
1470 let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1471 let expected_lambda = 1.0 / mean;
1472 assert!(
1473 (r.parameters[0].1 - expected_lambda).abs() < 1e-10,
1474 "λ̂ = {}, expected {}",
1475 r.parameters[0].1,
1476 expected_lambda
1477 );
1478 }
1479
1480 #[test]
1481 fn fit_exponential_edge_cases() {
1482 assert!(fit_exponential(&[1.0]).is_none()); assert!(fit_exponential(&[1.0, -1.0]).is_none()); assert!(fit_exponential(&[0.0, 1.0]).is_none()); assert!(fit_exponential(&[1.0, f64::NAN]).is_none());
1486 }
1487
1488 #[test]
1489 fn fit_gamma_basic() {
1490 let data = [
1492 1.5, 2.1, 1.8, 2.5, 1.9, 2.3, 2.0, 1.7, 2.4, 2.2, 1.6, 2.6, 1.4, 2.8, 2.1, 1.9, 2.3,
1493 2.0, 1.8, 2.5,
1494 ];
1495 let r = fit_gamma(&data).expect("should compute");
1496 assert_eq!(r.distribution, "Gamma");
1497 assert_eq!(r.n_params, 2);
1498 assert!(r.parameters[0].1 > 0.0, "α̂ > 0");
1499 assert!(r.parameters[1].1 > 0.0, "β̂ > 0");
1500 assert!(r.log_likelihood.is_finite());
1501 }
1502
1503 #[test]
1504 fn fit_gamma_edge_cases() {
1505 assert!(fit_gamma(&[1.0]).is_none()); assert!(fit_gamma(&[1.0, -1.0]).is_none()); assert!(fit_gamma(&[0.0, 1.0]).is_none()); assert!(fit_gamma(&[1.0, f64::NAN]).is_none());
1509 assert!(fit_gamma(&[5.0, 5.0, 5.0]).is_none()); }
1511
1512 #[test]
1513 fn fit_gamma_mean_rate_relationship() {
1514 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1515 let r = fit_gamma(&data).expect("should compute");
1516 let alpha = r.parameters[0].1;
1517 let beta = r.parameters[1].1;
1518 let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1519 assert!(
1521 (alpha / beta - mean).abs() < 1e-6,
1522 "α/β = {}, mean = {}",
1523 alpha / beta,
1524 mean
1525 );
1526 }
1527
1528 #[test]
1529 fn fit_best_sorted_by_aic() {
1530 let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7];
1531 let fits = fit_best(&data);
1532 assert!(!fits.is_empty());
1533 assert!(fits.len() >= 2, "got {} fits", fits.len());
1535 for w in fits.windows(2) {
1537 assert!(
1538 w[0].aic <= w[1].aic,
1539 "AIC not sorted: {} > {}",
1540 w[0].aic,
1541 w[1].aic
1542 );
1543 }
1544 }
1545
1546 #[test]
1547 fn fit_best_mixed_sign_data() {
1548 let data = [-2.0, -1.0, 0.5, 1.0, 2.0, 3.0, 1.5, -0.5];
1550 let fits = fit_best(&data);
1551 assert_eq!(fits.len(), 1, "only Normal should fit");
1552 assert_eq!(fits[0].distribution, "Normal");
1553 }
1554
1555 #[test]
1556 fn digamma_known_values() {
1557 assert!((digamma(1.0) + 0.5772156649).abs() < 1e-8);
1559 assert!((digamma(2.0) - 0.4227843351).abs() < 1e-8);
1561 assert!((digamma(0.5) + 1.9635100260).abs() < 1e-7);
1563 }
1564
1565 #[test]
1566 fn trigamma_known_values() {
1567 assert!((trigamma(1.0) - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-7);
1569 assert!((trigamma(2.0) - (std::f64::consts::PI.powi(2) / 6.0 - 1.0)).abs() < 1e-7);
1571 }
1572
1573 #[test]
1578 fn fit_lognormal_basic() {
1579 let data = [1.2, 2.5, 1.8, 3.1, 0.9, 2.0, 1.5, 4.0];
1580 let r = fit_lognormal(&data).expect("should compute");
1581 assert_eq!(r.distribution, "LogNormal");
1582 assert_eq!(r.n_params, 2);
1583 assert!(r.parameters[1].1 > 0.0); assert!(r.log_likelihood.is_finite());
1585 }
1586
1587 #[test]
1588 fn fit_lognormal_params_match_log_transform() {
1589 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1590 let r = fit_lognormal(&data).expect("should compute");
1591 let mu = r.parameters[0].1;
1592 let sigma = r.parameters[1].1;
1593 let log_data: Vec<f64> = data.iter().map(|&x| x.ln()).collect();
1595 let expected_mu: f64 = log_data.iter().sum::<f64>() / 5.0;
1596 assert!(
1597 (mu - expected_mu).abs() < 1e-10,
1598 "μ = {}, expected {}",
1599 mu,
1600 expected_mu
1601 );
1602 assert!(sigma > 0.0);
1603 }
1604
1605 #[test]
1606 fn fit_lognormal_edge_cases() {
1607 assert!(fit_lognormal(&[1.0]).is_none()); assert!(fit_lognormal(&[1.0, -1.0]).is_none()); assert!(fit_lognormal(&[0.0, 1.0]).is_none()); }
1611
1612 #[test]
1617 fn fit_poisson_basic() {
1618 let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 1.0, 2.0];
1619 let r = fit_poisson(&data).expect("should compute");
1620 assert_eq!(r.distribution, "Poisson");
1621 assert_eq!(r.n_params, 1);
1622 let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1623 assert!(
1624 (r.parameters[0].1 - mean).abs() < 1e-10,
1625 "λ̂ = {}, expected {}",
1626 r.parameters[0].1,
1627 mean
1628 );
1629 }
1630
1631 #[test]
1632 fn fit_poisson_edge_cases() {
1633 assert!(fit_poisson(&[1.0]).is_none()); assert!(fit_poisson(&[1.0, -1.0]).is_none()); assert!(fit_poisson(&[0.0, 0.0]).is_none()); assert!(fit_poisson(&[1.0, 2.5]).is_none()); }
1638
1639 #[test]
1644 fn fit_beta_basic() {
1645 let data = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
1646 let r = fit_beta(&data).expect("should compute");
1647 assert_eq!(r.distribution, "Beta");
1648 assert_eq!(r.n_params, 2);
1649 assert!(r.parameters[0].1 > 0.0, "α̂ > 0");
1650 assert!(r.parameters[1].1 > 0.0, "β̂ > 0");
1651 assert!(r.log_likelihood.is_finite());
1652 }
1653
1654 #[test]
1655 fn fit_beta_symmetric() {
1656 let data = [0.3, 0.4, 0.5, 0.6, 0.7, 0.35, 0.45, 0.55, 0.65, 0.5];
1658 let r = fit_beta(&data).expect("should compute");
1659 let alpha = r.parameters[0].1;
1660 let beta = r.parameters[1].1;
1661 assert!(
1663 (alpha - beta).abs() / alpha < 0.3,
1664 "α = {alpha}, β = {beta}, should be roughly equal"
1665 );
1666 }
1667
1668 #[test]
1669 fn fit_beta_edge_cases() {
1670 assert!(fit_beta(&[0.5]).is_none()); assert!(fit_beta(&[0.5, 1.0]).is_none()); assert!(fit_beta(&[0.0, 0.5]).is_none()); assert!(fit_beta(&[0.5, 1.5]).is_none()); assert!(fit_beta(&[0.5, -0.5]).is_none()); }
1676
1677 #[test]
1682 fn fit_best_compares_the_continuous_families_that_apply() {
1683 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1684 let fits = fit_best(&data);
1685 let names: Vec<&str> = fits.iter().map(|f| f.distribution.as_str()).collect();
1686 for family in ["Normal", "Exponential", "Gamma", "LogNormal", "Weibull"] {
1688 assert!(
1689 names.contains(&family),
1690 "should include {family}: {names:?}"
1691 );
1692 }
1693 assert!(!names.contains(&"Beta"), "{names:?}");
1694 assert!(!names.contains(&"Poisson"), "{names:?}");
1697
1698 let proportions = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
1699 let names: Vec<String> = fit_best(&proportions)
1700 .into_iter()
1701 .map(|f| f.distribution)
1702 .collect();
1703 assert!(names.iter().any(|n| n == "Beta"), "{names:?}");
1704 }
1705
1706 #[test]
1710 fn fit_best_ranking_does_not_depend_on_the_unit() {
1711 let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 5.0, 2.0, 3.0, 4.0];
1712 let in_thousandths: Vec<f64> = data.iter().map(|v| v * 1000.0).collect();
1713 let order = |d: &[f64]| -> Vec<String> {
1714 fit_best(d).into_iter().map(|f| f.distribution).collect()
1715 };
1716 assert_eq!(order(&data), order(&in_thousandths));
1717 }
1718
1719 #[test]
1720 fn fit_weibull_matches_the_weibull_mle() {
1721 let data = [12.0, 25.0, 31.0, 44.0, 52.0, 60.0, 71.0, 85.0];
1722 let fit = fit_weibull(&data).expect("positive data");
1723 let mle = crate::weibull::weibull_mle(&data).expect("same data");
1724 assert_eq!(fit.parameters[0], ("shape".to_string(), mle.shape));
1725 assert_eq!(fit.parameters[1], ("scale".to_string(), mle.scale));
1726 assert!((fit.aic - (-2.0 * mle.log_likelihood + 4.0)).abs() < 1e-12);
1727 assert!(fit_weibull(&[1.0, 0.0, 2.0]).is_none());
1728 }
1729}
1730
1731#[cfg(test)]
1732mod proptests {
1733 use super::*;
1734 use proptest::prelude::*;
1735
1736 proptest! {
1737 #[test]
1738 fn ecdf_last_is_one(
1739 data in proptest::collection::vec(-1e3_f64..1e3, 1..=50)
1740 ) {
1741 if let Some((_, probs)) = ecdf(&data) {
1742 let last = *probs.last().expect("non-empty");
1743 prop_assert!((last - 1.0).abs() < 1e-10, "last prob = {last}");
1744 }
1745 }
1746
1747 #[test]
1748 fn ecdf_monotonic(
1749 data in proptest::collection::vec(-1e3_f64..1e3, 2..=50)
1750 ) {
1751 if let Some((_, probs)) = ecdf(&data) {
1752 for w in probs.windows(2) {
1753 prop_assert!(w[1] >= w[0] - 1e-15, "{} < {}", w[1], w[0]);
1754 }
1755 }
1756 }
1757
1758 #[test]
1759 fn hist_counts_sum_to_n(
1760 data in proptest::collection::vec(-1e3_f64..1e3, 5..=100)
1761 ) {
1762 if let Some(r) = histogram_bins(&data, BinMethod::Sturges) {
1763 let total: usize = r.counts.iter().sum();
1764 prop_assert_eq!(total, data.len());
1765 }
1766 }
1767
1768 #[test]
1769 fn kde_density_non_negative(
1770 data in proptest::collection::vec(-100.0_f64..100.0, 5..=50)
1771 ) {
1772 if let Some(r) = kde(&data, BandwidthMethod::Silverman, 128) {
1773 for &d in &r.density {
1774 prop_assert!(d >= 0.0, "negative density: {d}");
1775 }
1776 }
1777 }
1778
1779 #[test]
1780 fn kde_integral_approx_one(
1781 data in proptest::collection::vec(-100.0_f64..100.0, 10..=80)
1782 ) {
1783 if let Some(r) = kde(&data, BandwidthMethod::Silverman, 512) {
1784 let dx = r.x[1] - r.x[0];
1785 let integral: f64 = r.density.iter().sum::<f64>() * dx;
1786 prop_assert!(
1787 (integral - 1.0).abs() < 0.1,
1788 "integral = {integral}, expected ≈ 1.0"
1789 );
1790 }
1791 }
1792
1793 #[test]
1794 fn fit_normal_aic_finite(
1795 data in proptest::collection::vec(-100.0_f64..100.0, 5..=50)
1796 ) {
1797 if let Some(r) = fit_normal(&data) {
1798 prop_assert!(r.aic.is_finite(), "AIC = {}", r.aic);
1799 prop_assert!(r.bic.is_finite(), "BIC = {}", r.bic);
1800 prop_assert!(r.log_likelihood.is_finite(), "LL = {}", r.log_likelihood);
1801 }
1802 }
1803
1804 #[test]
1805 fn fit_gamma_alpha_positive(
1806 data in proptest::collection::vec(0.1_f64..100.0, 5..=50)
1807 ) {
1808 if let Some(r) = fit_gamma(&data) {
1809 let alpha = r.parameters[0].1;
1810 let beta = r.parameters[1].1;
1811 prop_assert!(alpha > 0.0, "α = {alpha}");
1812 prop_assert!(beta > 0.0, "β = {beta}");
1813 prop_assert!(r.aic.is_finite(), "AIC = {}", r.aic);
1814 }
1815 }
1816 }
1817}