1use crate::compute::{ComputeEngine, CpuEngine};
4
5use crate::multivariate::context::MultiSeriesContext;
6use crate::multivariate::error::MultivariateError;
7
8#[derive(Debug, Clone)]
10pub struct MultivariateForecastResult {
11 pub timestamps: Vec<i64>,
13 pub series: Vec<String>,
25 pub predictions: Vec<Vec<f64>>,
27 pub target: String,
32 pub predictor_importance: Vec<(String, f64)>,
34}
35
36impl MultivariateForecastResult {
37 #[must_use]
39 pub fn for_series(&self, name: &str) -> Option<&[f64]> {
40 let i = self.series.iter().position(|s| s == name)?;
41 self.predictions.get(i).map(Vec::as_slice)
42 }
43
44 #[must_use]
49 pub fn target_forecast(&self) -> Option<&[f64]> {
50 self.for_series(&self.target)
51 }
52}
53
54pub trait MultivariateForecastModel: Send + Sync {
56 fn fit(&mut self, ctx: &MultiSeriesContext, target_idx: usize)
70 -> Result<(), MultivariateError>;
71
72 fn predict(&self, horizon: usize) -> Result<MultivariateForecastResult, MultivariateError>;
74
75 fn supports_online_learning(&self) -> bool;
77}
78
79pub struct MultiLinearRegression {
83 coefficients: Vec<f64>,
84 intercept: f64,
85 r_squared: f64,
86 target_idx: usize,
87 target_name: String,
89 predictor_names: Vec<String>,
90 last_predictors: Vec<f64>,
92 last_ts: i64,
93 interval_ns: i64,
94 fitted: bool,
95}
96
97impl MultiLinearRegression {
98 pub fn new() -> Self {
100 Self {
101 coefficients: Vec::new(),
102 intercept: 0.0,
103 r_squared: 0.0,
104 target_idx: 0,
105 target_name: String::new(),
106 predictor_names: Vec::new(),
107 last_predictors: Vec::new(),
108 last_ts: 0,
109 interval_ns: 1_000_000_000,
110 fitted: false,
111 }
112 }
113}
114
115impl Default for MultiLinearRegression {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl MultivariateForecastModel for MultiLinearRegression {
122 fn fit(
123 &mut self,
124 ctx: &MultiSeriesContext,
125 target_idx: usize,
126 ) -> Result<(), MultivariateError> {
127 let k = ctx.matrix.n_series();
128 let n = ctx.matrix.n_timestamps();
129 if target_idx >= k {
130 return Err(MultivariateError::InvalidParameter(format!(
131 "target_idx {target_idx} >= n_series {k}"
132 )));
133 }
134 let n_pred = k - 1;
135 if n < n_pred + 2 {
136 return Err(MultivariateError::InsufficientData {
137 min: n_pred + 2,
138 got: n,
139 });
140 }
141
142 self.target_idx = target_idx;
143 self.target_name = ctx.matrix.series_ids[target_idx].clone();
144 self.predictor_names = ctx
145 .matrix
146 .series_ids
147 .iter()
148 .enumerate()
149 .filter(|(i, _)| *i != target_idx)
150 .map(|(_, name)| name.clone())
151 .collect();
152
153 let y = &ctx.matrix.data[target_idx];
154 let p = n_pred + 1; let engine = CpuEngine::default();
158
159 let mut xtx = vec![0.0; p * p];
161 let mut xty = vec![0.0; p];
163
164 #[allow(clippy::needless_range_loop)]
165 for t in 0..n {
166 let mut row = Vec::with_capacity(p);
167 row.push(1.0); for s in 0..k {
169 if s != target_idx {
170 row.push(ctx.matrix.data[s][t]);
171 }
172 }
173 for i in 0..p {
174 for j in 0..p {
175 xtx[i * p + j] += row[i] * row[j];
176 }
177 xty[i] += row[i] * y[t];
178 }
179 }
180
181 for i in 0..p {
183 xtx[i * p + i] += 1e-8;
184 }
185
186 let beta = engine
188 .batch_matrix_solve(&xtx, &xty, p)
189 .map_err(|e: crate::compute::ComputeError| MultivariateError::Compute(e.to_string()))?;
190
191 self.intercept = beta[0];
192 self.coefficients = beta[1..].to_vec();
193
194 let y_mean = y.iter().sum::<f64>() / n as f64;
196 let ss_tot: f64 = y.iter().map(|&v| (v - y_mean).powi(2)).sum();
197 let mut ss_res = 0.0;
198 #[allow(clippy::needless_range_loop)]
199 for t in 0..n {
200 let mut pred = self.intercept;
201 let mut pidx = 0;
202 for s in 0..k {
203 if s != target_idx {
204 pred += self.coefficients[pidx] * ctx.matrix.data[s][t];
205 pidx += 1;
206 }
207 }
208 ss_res += (y[t] - pred).powi(2);
209 }
210 self.r_squared = if ss_tot > 1e-15 {
211 1.0 - ss_res / ss_tot
212 } else {
213 1.0
214 };
215
216 self.last_predictors = Vec::with_capacity(n_pred);
218 let last_t = n - 1;
219 for s in 0..k {
220 if s != target_idx {
221 self.last_predictors.push(ctx.matrix.data[s][last_t]);
222 }
223 }
224
225 self.last_ts = ctx
227 .matrix
228 .timestamps
229 .last()
230 .copied()
231 .ok_or(MultivariateError::InsufficientData { min: 1, got: 0 })?;
232 if ctx.matrix.timestamps.len() >= 2 {
233 let ts = &ctx.matrix.timestamps;
234 let mut deltas: Vec<i64> = ts.windows(2).map(|w| w[1] - w[0]).collect();
235 deltas.sort_unstable();
236 self.interval_ns = deltas[deltas.len() / 2];
237 }
238
239 self.fitted = true;
240 Ok(())
241 }
242
243 fn predict(&self, horizon: usize) -> Result<MultivariateForecastResult, MultivariateError> {
244 if !self.fitted {
245 return Err(MultivariateError::NotFitted);
246 }
247 let timestamps: Vec<i64> = (1..=horizon as i64)
250 .map(|h| {
251 self.last_ts
252 .saturating_add(h.saturating_mul(self.interval_ns))
253 })
254 .collect();
255
256 let pred_val = self.intercept
258 + self
259 .coefficients
260 .iter()
261 .zip(self.last_predictors.iter())
262 .map(|(c, x)| c * x)
263 .sum::<f64>();
264 let predictions = vec![vec![pred_val; horizon]];
265
266 let importance: Vec<(String, f64)> = self
267 .predictor_names
268 .iter()
269 .zip(self.coefficients.iter())
270 .map(|(name, &coef)| (name.clone(), coef.abs()))
271 .collect();
272
273 Ok(MultivariateForecastResult {
274 timestamps,
275 series: vec![self.target_name.clone()],
276 predictions,
277 target: self.target_name.clone(),
278 predictor_importance: importance,
279 })
280 }
281
282 fn supports_online_learning(&self) -> bool {
283 true
284 }
285}
286
287fn hessenberg_reduce(h: &mut [f64], n: usize) {
295 for col in 0..n.saturating_sub(2) {
296 let m = n - col - 1;
298 let mut x = vec![0.0; m];
299 for i in 0..m {
300 x[i] = h[(col + 1 + i) * n + col];
301 }
302
303 let x_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
304 if x_norm < 1e-15 {
305 continue;
306 }
307
308 let sign = if x[0] >= 0.0 { 1.0 } else { -1.0 };
309 let alpha = -sign * x_norm;
310 x[0] -= alpha;
311 let v_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
312 if v_norm < 1e-15 {
313 continue;
314 }
315 for v in &mut x {
316 *v /= v_norm;
317 }
318
319 for j in 0..n {
321 let mut dot = 0.0;
322 for i in 0..m {
323 dot += x[i] * h[(col + 1 + i) * n + j];
324 }
325 for i in 0..m {
326 h[(col + 1 + i) * n + j] -= 2.0 * x[i] * dot;
327 }
328 }
329
330 for i in 0..n {
332 let mut dot = 0.0;
333 for j in 0..m {
334 dot += h[i * n + col + 1 + j] * x[j];
335 }
336 for j in 0..m {
337 h[i * n + col + 1 + j] -= 2.0 * dot * x[j];
338 }
339 }
340 }
341}
342
343fn qr_francis(h: &mut [f64], n: usize) {
346 if n <= 1 {
347 return;
348 }
349
350 let max_iter = 100 * n;
351 let mut iter_count = 0;
352 let mut nn = n;
353
354 while nn > 1 && iter_count < max_iter {
355 iter_count += 1;
356
357 let sub = h[(nn - 1) * n + nn - 2];
359 let diag_sum = h[(nn - 2) * n + nn - 2].abs() + h[(nn - 1) * n + nn - 1].abs();
360 let tol = 1e-14 * diag_sum.max(1e-15);
361
362 if sub.abs() <= tol {
363 nn -= 1;
364 continue;
365 }
366
367 if nn > 2 {
369 let sub2 = h[(nn - 2) * n + nn - 3];
370 let diag_sum2 = h[(nn - 3) * n + nn - 3].abs() + h[(nn - 2) * n + nn - 2].abs();
371 let tol2 = 1e-14 * diag_sum2.max(1e-15);
372 if sub2.abs() <= tol2 {
373 nn -= 2;
375 continue;
376 }
377 } else {
378 break;
380 }
381
382 let a11 = h[(nn - 2) * n + nn - 2];
384 let a12 = h[(nn - 2) * n + nn - 1];
385 let a21 = h[(nn - 1) * n + nn - 2];
386 let a22 = h[(nn - 1) * n + nn - 1];
387 let trace = a11 + a22;
388 let det = a11 * a22 - a12 * a21;
389
390 let shift = a22;
392 h[0] -= shift;
393 for i in 0..n {
394 if i < nn {
395 h[i * n + i] -= shift;
396 }
397 }
398 for i in 0..n {
403 if i < nn {
404 h[i * n + i] += shift;
405 }
406 }
407 h[0] += shift;
408
409 let _ = trace; let _ = det;
412 qr_step_givens(h, n, nn, a22);
413 }
414}
415
416fn qr_step_givens(h: &mut [f64], n: usize, nn: usize, shift: f64) {
420 for i in 0..nn {
422 h[i * n + i] -= shift;
423 }
424
425 let mut cs = vec![0.0; nn - 1];
427 let mut sn = vec![0.0; nn - 1];
428
429 for i in 0..nn - 1 {
430 let a = h[i * n + i];
431 let b = h[(i + 1) * n + i];
432 let r = a.hypot(b);
433 if r < 1e-30 {
434 cs[i] = 1.0;
435 sn[i] = 0.0;
436 continue;
437 }
438 cs[i] = a / r;
439 sn[i] = b / r;
440
441 for j in 0..n {
443 let t1 = h[i * n + j];
444 let t2 = h[(i + 1) * n + j];
445 h[i * n + j] = cs[i] * t1 + sn[i] * t2;
446 h[(i + 1) * n + j] = -sn[i] * t1 + cs[i] * t2;
447 }
448 }
449
450 for i in 0..nn - 1 {
452 for j in 0..n {
453 let t1 = h[j * n + i];
454 let t2 = h[j * n + i + 1];
455 h[j * n + i] = cs[i] * t1 + sn[i] * t2;
456 h[j * n + i + 1] = -sn[i] * t1 + cs[i] * t2;
457 }
458 }
459
460 for i in 0..nn {
462 h[i * n + i] += shift;
463 }
464}
465
466fn spectral_radius_from_schur(h: &[f64], n: usize) -> f64 {
469 let mut max_mag = 0.0f64;
470 let mut i = 0;
471
472 while i < n {
473 if i + 1 < n
474 && h[(i + 1) * n + i].abs()
475 > 1e-14 * (h[i * n + i].abs() + h[(i + 1) * n + i + 1].abs()).max(1e-15)
476 {
477 let a = h[i * n + i];
479 let b = h[i * n + i + 1];
480 let c = h[(i + 1) * n + i];
481 let d = h[(i + 1) * n + i + 1];
482 let det = a * d - b * c;
487 let trace = a + d;
488 let disc = trace * trace - 4.0 * det;
489 let mag = if disc < 0.0 {
490 det.abs().sqrt()
492 } else {
493 let sq = disc.sqrt();
495 let e1 = (trace + sq) / 2.0;
496 let e2 = (trace - sq) / 2.0;
497 e1.abs().max(e2.abs())
498 };
499 max_mag = max_mag.max(mag);
500 i += 2;
501 } else {
502 max_mag = max_mag.max(h[i * n + i].abs());
504 i += 1;
505 }
506 }
507
508 max_mag
509}
510
511pub struct VarModel {
520 lag_order: usize,
521 coefficients: Vec<Vec<f64>>,
523 intercepts: Vec<f64>,
524 k: usize,
525 last_values: Vec<Vec<f64>>, last_ts: i64,
527 interval_ns: i64,
528 series_names: Vec<String>,
529 target_name: String,
534 fitted: bool,
535 stability_threshold: f64,
537}
538
539impl VarModel {
540 pub fn new(lag_order: Option<usize>) -> Self {
542 Self::new_with_stability_threshold(lag_order, None)
543 }
544
545 pub fn new_with_stability_threshold(
552 lag_order: Option<usize>,
553 stability_threshold: Option<f64>,
554 ) -> Self {
555 Self {
556 lag_order: lag_order.unwrap_or(1),
557 coefficients: Vec::new(),
558 intercepts: Vec::new(),
559 k: 0,
560 last_values: Vec::new(),
561 last_ts: 0,
562 interval_ns: 1_000_000_000,
563 series_names: Vec::new(),
564 target_name: String::new(),
565 fitted: false,
566 stability_threshold: stability_threshold.unwrap_or(1.05),
567 }
568 }
569
570 fn compute_spectral_radius(&self) -> f64 {
580 let k = self.k;
581 let p = self.lag_order;
582 let n = p * k;
583 if n == 0 {
584 return 0.0;
585 }
586
587 let mut h = vec![0.0; n * n];
596
597 for i in 0..k {
599 for l in 0..p {
600 for j in 0..k {
601 h[i * n + l * k + j] = self.coefficients[i][l * k + j];
602 }
603 }
604 }
605
606 for i in k..n {
608 h[i * n + (i - k)] = 1.0;
609 }
610
611 hessenberg_reduce(&mut h, n);
614
615 qr_francis(&mut h, n);
618
619 spectral_radius_from_schur(&h, n)
621 }
622}
623
624impl MultivariateForecastModel for VarModel {
625 fn fit(
626 &mut self,
627 ctx: &MultiSeriesContext,
628 target_idx: usize,
629 ) -> Result<(), MultivariateError> {
630 let k = ctx.matrix.n_series();
631 let n = ctx.matrix.n_timestamps();
632 let p = self.lag_order;
633
634 if target_idx >= k {
635 return Err(MultivariateError::InvalidParameter(format!(
636 "target_idx {target_idx} >= n_series {k}"
637 )));
638 }
639 self.target_name = ctx.matrix.series_ids[target_idx].clone();
640
641 if n <= p + 1 {
642 return Err(MultivariateError::InsufficientData { min: p + 2, got: n });
643 }
644
645 self.k = k;
646 self.series_names = ctx.matrix.series_ids.clone();
647
648 let engine = CpuEngine::default();
649
650 let dim = 1 + p * k; self.intercepts = vec![0.0; k];
654 self.coefficients = vec![vec![0.0; p * k]; k]; for eq in 0..k {
657 let mut xtx = vec![0.0; dim * dim];
658 let mut xty = vec![0.0; dim];
659
660 for t in p..n {
661 let mut row = Vec::with_capacity(dim);
662 row.push(1.0); for l in 1..=p {
664 for s in 0..k {
665 row.push(ctx.matrix.data[s][t - l]);
666 }
667 }
668 let y = ctx.matrix.data[eq][t];
669 for i in 0..dim {
670 for j in 0..dim {
671 xtx[i * dim + j] += row[i] * row[j];
672 }
673 xty[i] += row[i] * y;
674 }
675 }
676
677 for i in 0..dim {
679 xtx[i * dim + i] += 1e-8;
680 }
681
682 let beta = engine.batch_matrix_solve(&xtx, &xty, dim).map_err(
683 |e: crate::compute::ComputeError| MultivariateError::Compute(e.to_string()),
684 )?;
685
686 self.intercepts[eq] = beta[0];
687 self.coefficients[eq] = beta[1..].to_vec();
688 }
689
690 self.last_values = Vec::with_capacity(p);
692 for l in 0..p {
693 let t = n - p + l;
694 let obs: Vec<f64> = (0..k).map(|s| ctx.matrix.data[s][t]).collect();
695 self.last_values.push(obs);
696 }
697
698 self.last_ts = ctx
700 .matrix
701 .timestamps
702 .last()
703 .copied()
704 .ok_or(MultivariateError::InsufficientData { min: 1, got: 0 })?;
705 if ctx.matrix.timestamps.len() >= 2 {
706 let ts = &ctx.matrix.timestamps;
707 let mut deltas: Vec<i64> = ts.windows(2).map(|w| w[1] - w[0]).collect();
708 deltas.sort_unstable();
709 self.interval_ns = deltas[deltas.len() / 2];
710 }
711
712 self.fitted = true;
713
714 let stability_threshold = self.stability_threshold;
720 let spectral_radius = self.compute_spectral_radius();
721 if spectral_radius >= stability_threshold {
722 tracing::warn!(
723 spectral_radius = spectral_radius,
724 "VAR model is unstable (spectral radius >= {stability_threshold}) — forecasts may diverge"
725 );
726 return Err(MultivariateError::InvalidParameter(format!(
727 "VAR model is unstable: spectral radius {spectral_radius:.4} >= {stability_threshold}; \
728 try increasing regularization, reducing lag order, or differencing the data"
729 )));
730 } else if spectral_radius >= 1.0 {
731 tracing::warn!(
732 spectral_radius = spectral_radius,
733 "VAR model is near unit-root (spectral radius close to 1.0) — \
734 forecasts may drift; consider differencing"
735 );
736 }
737
738 Ok(())
739 }
740
741 fn predict(&self, horizon: usize) -> Result<MultivariateForecastResult, MultivariateError> {
742 if !self.fitted {
743 return Err(MultivariateError::NotFitted);
744 }
745
746 let k = self.k;
747 let p = self.lag_order;
748 let mut history = self.last_values.clone();
749 let mut predictions = vec![Vec::with_capacity(horizon); k];
750 let timestamps: Vec<i64> = (1..=horizon as i64)
751 .map(|h| {
752 self.last_ts
753 .saturating_add(h.saturating_mul(self.interval_ns))
754 })
755 .collect();
756
757 for _ in 0..horizon {
758 let mut new_obs = vec![0.0; k];
759 #[allow(clippy::needless_range_loop)]
760 for eq in 0..k {
761 let mut val = self.intercepts[eq];
762 #[allow(clippy::needless_range_loop)]
763 for l in 0..p {
764 let lag_idx = history.len() - 1 - l;
765 if lag_idx < history.len() {
766 for s in 0..k {
767 val += self.coefficients[eq][l * k + s] * history[lag_idx][s];
768 }
769 }
770 }
771 new_obs[eq] = val;
772 }
773 for eq in 0..k {
774 predictions[eq].push(new_obs[eq]);
775 }
776 history.push(new_obs);
777 }
778
779 let mut importance = Vec::with_capacity(k);
781 for s in 0..k {
782 let avg: f64 = (0..k)
783 .flat_map(|eq| (0..p).map(move |l| self.coefficients[eq][l * k + s].abs()))
784 .sum::<f64>()
785 / (k * p) as f64;
786 importance.push((self.series_names[s].clone(), avg));
787 }
788
789 Ok(MultivariateForecastResult {
790 timestamps,
791 series: self.series_names.clone(),
792 predictions,
793 target: self.target_name.clone(),
794 predictor_importance: importance,
795 })
796 }
797
798 fn supports_online_learning(&self) -> bool {
799 false
800 }
801}
802
803#[derive(Debug, Clone)]
807pub struct GrangerCausalityResult {
808 pub cause: String,
810 pub effect: String,
812 pub lag_order: usize,
814 pub f_statistic: f64,
816 pub p_value: f64,
818 pub adjusted_p_value: f64,
821 pub significant: bool,
823 pub rss_unrestricted: f64,
825 pub rss_restricted: f64,
827}
828
829impl VarModel {
830 pub fn granger_causality(
846 &self,
847 ctx: &MultiSeriesContext,
848 ) -> Result<Vec<GrangerCausalityResult>, MultivariateError> {
849 if !self.fitted {
850 return Err(MultivariateError::NotFitted);
851 }
852
853 let k = self.k;
854 let p = self.lag_order;
855 let n = ctx.matrix.n_timestamps();
856 let t = n - p; if t <= 1 + p * k {
859 return Err(MultivariateError::InsufficientData {
860 min: 2 + p * k,
861 got: t,
862 });
863 }
864
865 let engine = CpuEngine::default();
866 let dim_full = 1 + p * k; let dim_restricted = 1 + p * (k - 1); let mut results = Vec::with_capacity(k * (k - 1));
870
871 for effect_eq in 0..k {
872 for cause_s in 0..k {
873 if cause_s == effect_eq {
874 continue;
875 }
876
877 let rss_u = self.ols_rss(ctx, &engine, effect_eq, None, dim_full, p, k)?;
879
880 let rss_r =
882 self.ols_rss(ctx, &engine, effect_eq, Some(cause_s), dim_restricted, p, k)?;
883
884 let df1 = p as f64; let df2 = (t - dim_full) as f64; let f_stat = if rss_u.abs() < 1e-30 {
888 0.0
889 } else {
890 ((rss_r - rss_u) / df1) / (rss_u / df2)
891 };
892
893 let p_value = f_distribution_sf(f_stat, df1, df2);
897
898 results.push(GrangerCausalityResult {
899 cause: self.series_names[cause_s].clone(),
900 effect: self.series_names[effect_eq].clone(),
901 lag_order: p,
902 f_statistic: f_stat,
903 p_value,
904 adjusted_p_value: p_value, significant: false, rss_unrestricted: rss_u,
907 rss_restricted: rss_r,
908 });
909 }
910 }
911
912 let m = results.len();
916 if m > 0 {
917 let alpha = 0.05;
918 let mut indices: Vec<usize> = (0..m).collect();
919 indices.sort_by(|&a, &b| {
920 results[a]
921 .p_value
922 .partial_cmp(&results[b].p_value)
923 .unwrap_or(std::cmp::Ordering::Equal)
924 });
925
926 let mut adj_p = vec![0.0; m];
929 for (rank, &idx) in indices.iter().enumerate() {
930 adj_p[idx] = (results[idx].p_value * m as f64 / (rank + 1) as f64).min(1.0);
931 }
932 let mut running_min = 1.0;
934 for &idx in indices.iter().rev() {
935 adj_p[idx] = adj_p[idx].min(running_min);
936 running_min = adj_p[idx];
937 }
938 for (i, p_adj) in adj_p.into_iter().enumerate() {
939 results[i].adjusted_p_value = p_adj;
940 results[i].significant = p_adj < alpha;
941 }
942 }
943
944 Ok(results)
945 }
946
947 pub fn granger_causality_robust(
962 &self,
963 ctx: &MultiSeriesContext,
964 ) -> Result<Vec<GrangerCausalityResult>, MultivariateError> {
965 if !self.fitted {
966 return Err(MultivariateError::NotFitted);
967 }
968
969 let k = self.k;
970 let p = self.lag_order;
971 let n = ctx.matrix.n_timestamps();
972 let t = n - p;
973
974 if t <= 1 + p * k {
975 return Err(MultivariateError::InsufficientData {
976 min: 2 + p * k,
977 got: t,
978 });
979 }
980
981 let engine = CpuEngine::default();
982 let dim = 1 + p * k; let df_den = t as f64 - dim as f64;
984
985 if df_den <= 0.0 {
986 return Err(MultivariateError::InsufficientData {
987 min: dim + p + 1,
988 got: t,
989 });
990 }
991
992 let mut results = Vec::with_capacity(k * (k - 1));
993
994 for effect_eq in 0..k {
995 let mut x_mat = vec![0.0_f64; t * dim];
997 let mut y_vec = vec![0.0_f64; t];
998
999 for (row, obs) in (p..n).enumerate() {
1000 y_vec[row] = ctx.matrix.data[effect_eq][obs];
1001 x_mat[row * dim] = 1.0;
1002 let mut col = 1;
1003 for l in 1..=p {
1004 for s in 0..k {
1005 x_mat[row * dim + col] = ctx.matrix.data[s][obs - l];
1006 col += 1;
1007 }
1008 }
1009 }
1010
1011 let mut xtx = vec![0.0_f64; dim * dim];
1013 for row in 0..t {
1014 for i in 0..dim {
1015 let xi = x_mat[row * dim + i];
1016 for j in i..dim {
1017 let v = xi * x_mat[row * dim + j];
1018 xtx[i * dim + j] += v;
1019 if i != j {
1020 xtx[j * dim + i] += v;
1021 }
1022 }
1023 }
1024 }
1025 let mut xty = vec![0.0_f64; dim];
1027 for row in 0..t {
1028 let y = y_vec[row];
1029 for i in 0..dim {
1030 xty[i] += x_mat[row * dim + i] * y;
1031 }
1032 }
1033 for i in 0..dim {
1035 xtx[i * dim + i] += 1e-8;
1036 }
1037
1038 let beta = engine
1040 .batch_matrix_solve(&xtx, &xty, dim)
1041 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
1042
1043 let mut residuals = vec![0.0_f64; t];
1045 for row in 0..t {
1046 let mut fitted = 0.0;
1047 for j in 0..dim {
1048 fitted += x_mat[row * dim + j] * beta[j];
1049 }
1050 residuals[row] = y_vec[row] - fitted;
1051 }
1052
1053 let mut xtx_inv = vec![0.0_f64; dim * dim];
1055 for c in 0..dim {
1056 let mut rhs = vec![0.0_f64; dim];
1057 rhs[c] = 1.0;
1058 let sol = engine
1059 .batch_matrix_solve(&xtx, &rhs, dim)
1060 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
1061 for r in 0..dim {
1062 xtx_inv[r * dim + c] = sol[r];
1063 }
1064 }
1065
1066 let mut hat_diag = vec![0.0_f64; t];
1068 for row in 0..t {
1069 let mut h = 0.0;
1070 for i in 0..dim {
1071 let mut inner = 0.0;
1072 for j in 0..dim {
1073 inner += xtx_inv[i * dim + j] * x_mat[row * dim + j];
1074 }
1075 h += x_mat[row * dim + i] * inner;
1076 }
1077 hat_diag[row] = h.clamp(0.0, 1.0 - 1e-12);
1078 }
1079
1080 let mut meat = vec![0.0_f64; dim * dim];
1082 for row in 0..t {
1083 let denom = (1.0 - hat_diag[row]).powi(2);
1084 let w = residuals[row].powi(2) / denom;
1085 for i in 0..dim {
1086 let xi_w = x_mat[row * dim + i] * w;
1087 for j in i..dim {
1088 let v = xi_w * x_mat[row * dim + j];
1089 meat[i * dim + j] += v;
1090 if i != j {
1091 meat[j * dim + i] += v;
1092 }
1093 }
1094 }
1095 }
1096
1097 let mut tmp = vec![0.0_f64; dim * dim];
1099 for i in 0..dim {
1100 for j in 0..dim {
1101 let mut s = 0.0;
1102 for c in 0..dim {
1103 s += xtx_inv[i * dim + c] * meat[c * dim + j];
1104 }
1105 tmp[i * dim + j] = s;
1106 }
1107 }
1108 let mut v_hc3 = vec![0.0_f64; dim * dim];
1109 for i in 0..dim {
1110 for j in 0..dim {
1111 let mut s = 0.0;
1112 for c in 0..dim {
1113 s += tmp[i * dim + c] * xtx_inv[c * dim + j];
1114 }
1115 v_hc3[i * dim + j] = s;
1116 }
1117 }
1118
1119 let rss_u: f64 = residuals.iter().map(|e| e * e).sum();
1121
1122 for cause_s in 0..k {
1124 if cause_s == effect_eq {
1125 continue;
1126 }
1127
1128 let cause_cols: Vec<usize> = (1..=p).map(|l| 1 + (l - 1) * k + cause_s).collect();
1131
1132 let r_beta: Vec<f64> = cause_cols.iter().map(|&c| beta[c]).collect();
1134
1135 let mut r_v_rt = vec![0.0_f64; p * p];
1137 for (i, &ci) in cause_cols.iter().enumerate() {
1138 for (j, &cj) in cause_cols.iter().enumerate() {
1139 r_v_rt[i * p + j] = v_hc3[ci * dim + cj];
1140 }
1141 }
1142 for i in 0..p {
1143 r_v_rt[i * p + i] += 1e-12;
1144 }
1145
1146 let inv_r_beta = engine
1148 .batch_matrix_solve(&r_v_rt, &r_beta, p)
1149 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
1150
1151 let wald: f64 = r_beta
1152 .iter()
1153 .zip(inv_r_beta.iter())
1154 .map(|(a, b)| a * b)
1155 .sum();
1156 let f_stat = (wald / p as f64).max(0.0);
1157 let p_value = f_distribution_sf(f_stat, p as f64, df_den);
1158
1159 let dim_r = 1 + p * (k - 1);
1161 let rss_r = self.ols_rss(ctx, &engine, effect_eq, Some(cause_s), dim_r, p, k)?;
1162
1163 results.push(GrangerCausalityResult {
1164 cause: self.series_names[cause_s].clone(),
1165 effect: self.series_names[effect_eq].clone(),
1166 lag_order: p,
1167 f_statistic: f_stat,
1168 p_value,
1169 adjusted_p_value: p_value,
1170 significant: false,
1171 rss_unrestricted: rss_u,
1172 rss_restricted: rss_r,
1173 });
1174 }
1175 }
1176
1177 let m = results.len();
1179 if m > 0 {
1180 let alpha = 0.05;
1181 let mut indices: Vec<usize> = (0..m).collect();
1182 indices.sort_by(|&a, &b| {
1183 results[a]
1184 .p_value
1185 .partial_cmp(&results[b].p_value)
1186 .unwrap_or(std::cmp::Ordering::Equal)
1187 });
1188 let mut adj_p = vec![0.0; m];
1189 for (rank, &idx) in indices.iter().enumerate() {
1190 adj_p[idx] = (results[idx].p_value * m as f64 / (rank + 1) as f64).min(1.0);
1191 }
1192 let mut running_min = 1.0;
1193 for &idx in indices.iter().rev() {
1194 adj_p[idx] = adj_p[idx].min(running_min);
1195 running_min = adj_p[idx];
1196 }
1197 for (i, p_adj) in adj_p.into_iter().enumerate() {
1198 results[i].adjusted_p_value = p_adj;
1199 results[i].significant = p_adj < alpha;
1200 }
1201 }
1202
1203 Ok(results)
1204 }
1205
1206 fn ols_rss(
1211 &self,
1212 ctx: &MultiSeriesContext,
1213 engine: &CpuEngine,
1214 effect_eq: usize,
1215 exclude_series: Option<usize>,
1216 dim: usize,
1217 p: usize,
1218 k: usize,
1219 ) -> Result<f64, MultivariateError> {
1220 let n = ctx.matrix.n_timestamps();
1221 let mut xtx = vec![0.0; dim * dim];
1222 let mut xty = vec![0.0; dim];
1223
1224 for obs in p..n {
1225 let mut row = Vec::with_capacity(dim);
1226 row.push(1.0); for l in 1..=p {
1228 for s in 0..k {
1229 if exclude_series == Some(s) {
1230 continue;
1231 }
1232 row.push(ctx.matrix.data[s][obs - l]);
1233 }
1234 }
1235 let y = ctx.matrix.data[effect_eq][obs];
1236 for i in 0..dim {
1237 for j in 0..dim {
1238 xtx[i * dim + j] += row[i] * row[j];
1239 }
1240 xty[i] += row[i] * y;
1241 }
1242 }
1243
1244 for i in 0..dim {
1246 xtx[i * dim + i] += 1e-8;
1247 }
1248
1249 let beta = engine
1250 .batch_matrix_solve(&xtx, &xty, dim)
1251 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
1252
1253 let mut rss = 0.0;
1255 for obs in p..n {
1256 let mut pred = beta[0]; let mut col = 1;
1258 for l in 1..=p {
1259 for s in 0..k {
1260 if exclude_series == Some(s) {
1261 continue;
1262 }
1263 pred += beta[col] * ctx.matrix.data[s][obs - l];
1264 col += 1;
1265 }
1266 }
1267 let residual = ctx.matrix.data[effect_eq][obs] - pred;
1268 rss += residual * residual;
1269 }
1270
1271 Ok(rss)
1272 }
1273}
1274
1275fn f_distribution_sf(f: f64, d1: f64, d2: f64) -> f64 {
1280 if f <= 0.0 || d1 <= 0.0 || d2 <= 0.0 {
1281 return 1.0;
1282 }
1283 let z = d2 / (d2 + d1 * f);
1284 regularized_incomplete_beta(z, d2 / 2.0, d1 / 2.0)
1285}
1286
1287fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
1290 if x <= 0.0 {
1291 return 0.0;
1292 }
1293 if x >= 1.0 {
1294 return 1.0;
1295 }
1296
1297 if x > (a + 1.0) / (a + b + 2.0) {
1300 return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
1301 }
1302
1303 let ln_prefix = a * x.ln() + b * (1.0 - x).ln() - ln_beta(a, b) - a.ln();
1304 let prefix = ln_prefix.exp();
1305
1306 let max_iter = 200;
1308 let eps = 1e-14;
1309 let tiny = 1e-30;
1310
1311 let mut c = 1.0;
1312 let mut d = 1.0 - (a + b) * x / (a + 1.0);
1313 if d.abs() < tiny {
1314 d = tiny;
1315 }
1316 d = 1.0 / d;
1317 let mut h = d;
1318
1319 for m in 1..=max_iter {
1320 let m_f64 = m as f64;
1321
1322 let num_even = m_f64 * (b - m_f64) * x / ((a + 2.0 * m_f64 - 1.0) * (a + 2.0 * m_f64));
1324 d = 1.0 + num_even * d;
1325 if d.abs() < tiny {
1326 d = tiny;
1327 }
1328 c = 1.0 + num_even / c;
1329 if c.abs() < tiny {
1330 c = tiny;
1331 }
1332 d = 1.0 / d;
1333 h *= d * c;
1334
1335 let num_odd =
1337 -((a + m_f64) * (a + b + m_f64)) * x / ((a + 2.0 * m_f64) * (a + 2.0 * m_f64 + 1.0));
1338 d = 1.0 + num_odd * d;
1339 if d.abs() < tiny {
1340 d = tiny;
1341 }
1342 c = 1.0 + num_odd / c;
1343 if c.abs() < tiny {
1344 c = tiny;
1345 }
1346 d = 1.0 / d;
1347 let delta = d * c;
1348 h *= delta;
1349
1350 if (delta - 1.0).abs() < eps {
1351 break;
1352 }
1353 }
1354
1355 prefix * h
1356}
1357
1358fn ln_beta(a: f64, b: f64) -> f64 {
1360 use crate::forecast::diagnostics::ln_gamma;
1361 ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
1362}
1363#[cfg(test)]
1364mod tests {
1365 use super::*;
1366
1367 fn simple_ctx() -> MultiSeriesContext {
1368 let n = 100;
1369 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1370 let x1: Vec<f64> = (0..n).map(|i| i as f64).collect();
1372 let x2: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin() * 10.0).collect();
1373 let y: Vec<f64> = (0..n)
1375 .map(|i| 2.0 * (i as f64) + 3.0 * (i as f64 * 0.1).sin() * 10.0 + 5.0)
1376 .collect();
1377 MultiSeriesContext::build(
1378 vec![
1379 ("y".to_string(), ts.clone(), y),
1380 ("x1".to_string(), ts.clone(), x1),
1381 ("x2".to_string(), ts, x2),
1382 ],
1383 None,
1384 )
1385 .unwrap()
1386 }
1387
1388 #[test]
1389 fn multi_linear_regression_fit() {
1390 let ctx = simple_ctx();
1391 let mut model = MultiLinearRegression::new();
1392 model.fit(&ctx, 0).unwrap();
1393 assert!(model.r_squared > 0.99, "R²={}", model.r_squared);
1394 assert!((model.coefficients[0] - 2.0).abs() < 0.5);
1396 assert!((model.coefficients[1] - 3.0).abs() < 0.5);
1397 }
1398
1399 #[test]
1400 fn multi_linear_regression_predict() {
1401 let ctx = simple_ctx();
1402 let mut model = MultiLinearRegression::new();
1403 model.fit(&ctx, 0).unwrap();
1404 let result = model.predict(5).unwrap();
1405 assert_eq!(result.predictions[0].len(), 5);
1406 assert_eq!(result.timestamps.len(), 5);
1407 let pred = result.predictions[0][0];
1410 assert!(
1411 pred > 100.0,
1412 "prediction {} should reflect learned coefficients, not just intercept",
1413 pred
1414 );
1415 }
1416
1417 #[test]
1418 fn var_fit_and_predict() {
1419 let n = 200;
1420 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1421 let s1: Vec<f64> = (0..n)
1422 .map(|i| (i as f64 * 0.05).sin() * 10.0 + 50.0)
1423 .collect();
1424 let s2: Vec<f64> = (0..n)
1425 .map(|i| (i as f64 * 0.05).cos() * 10.0 + 50.0)
1426 .collect();
1427 let ctx = MultiSeriesContext::build(
1428 vec![
1429 ("s1".to_string(), ts.clone(), s1),
1430 ("s2".to_string(), ts, s2),
1431 ],
1432 None,
1433 )
1434 .unwrap();
1435
1436 let mut model = VarModel::new(Some(2));
1437 model.fit(&ctx, 0).unwrap();
1438 let result = model.predict(10).unwrap();
1439 assert_eq!(result.predictions.len(), 2); assert_eq!(result.predictions[0].len(), 10);
1441 assert_eq!(result.predictions[1].len(), 10);
1442 }
1443
1444 #[test]
1445 fn var_predictor_importance() {
1446 let n = 200;
1447 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1448 let s1: Vec<f64> = (0..n).map(|i| i as f64).collect();
1449 let s2: Vec<f64> = (0..n).map(|i| i as f64 * 2.0).collect();
1450 let ctx = MultiSeriesContext::build(
1451 vec![
1452 ("s1".to_string(), ts.clone(), s1),
1453 ("s2".to_string(), ts, s2),
1454 ],
1455 None,
1456 )
1457 .unwrap();
1458
1459 let mut model = VarModel::new(Some(1));
1460 model.fit(&ctx, 0).unwrap();
1461 let result = model.predict(5).unwrap();
1462 assert_eq!(result.predictor_importance.len(), 2);
1463 }
1464
1465 #[test]
1466 fn granger_causality_detects_cause() {
1467 let n = 300;
1469 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1470 let mut rng_state: u64 = 42;
1472 let mut pseudo_rand = || -> f64 {
1473 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
1474 ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5
1475 };
1476
1477 let s1: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin() * 5.0).collect();
1478 let mut s2 = vec![0.0; n];
1479 for i in 1..n {
1480 s2[i] = 0.8 * s1[i - 1] + pseudo_rand();
1481 }
1482
1483 let ctx = MultiSeriesContext::build(
1484 vec![
1485 ("s1".to_string(), ts.clone(), s1),
1486 ("s2".to_string(), ts, s2),
1487 ],
1488 None,
1489 )
1490 .unwrap();
1491
1492 let mut model = VarModel::new(Some(2));
1493 model.fit(&ctx, 0).unwrap();
1494
1495 let results = model.granger_causality(&ctx).unwrap();
1496 assert_eq!(results.len(), 2); let s1_causes_s2 = results
1499 .iter()
1500 .find(|r| r.cause == "s1" && r.effect == "s2")
1501 .unwrap();
1502 assert!(
1503 s1_causes_s2.significant,
1504 "s1 should Granger-cause s2: F={:.3}, p={:.4}",
1505 s1_causes_s2.f_statistic, s1_causes_s2.p_value
1506 );
1507 assert!(s1_causes_s2.f_statistic > 1.0);
1508 assert!(s1_causes_s2.p_value < 0.05);
1509 }
1510
1511 #[test]
1512 fn granger_requires_fitted_model() {
1513 let n = 100;
1514 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1515 let s1: Vec<f64> = (0..n).map(|i| i as f64).collect();
1516 let s2: Vec<f64> = (0..n).map(|i| (i as f64).sqrt()).collect();
1517 let ctx = MultiSeriesContext::build(
1518 vec![
1519 ("s1".to_string(), ts.clone(), s1),
1520 ("s2".to_string(), ts, s2),
1521 ],
1522 None,
1523 )
1524 .unwrap();
1525
1526 let model = VarModel::new(Some(1));
1527 let err = model.granger_causality(&ctx);
1528 assert!(err.is_err());
1529 }
1530
1531 #[test]
1532 fn granger_robust_detects_cause() {
1533 let n = 200;
1535 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1536 let mut s1 = vec![0.0; n];
1537 let mut s2 = vec![0.0; n];
1538 let mut rng_state: u64 = 12345;
1539 for i in 1..n {
1540 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
1541 let noise1 = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5;
1542 s1[i] = 0.7 * s1[i - 1] + noise1;
1543 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
1544 let noise2 = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.2;
1545 s2[i] = 0.8 * s1[i - 1] + noise2;
1547 }
1548 let ctx = MultiSeriesContext::build(
1549 vec![
1550 ("s1".to_string(), ts.clone(), s1),
1551 ("s2".to_string(), ts, s2),
1552 ],
1553 None,
1554 )
1555 .unwrap();
1556
1557 let mut model = VarModel::new(Some(1));
1558 model.fit(&ctx, 1).unwrap();
1559 let results = model.granger_causality_robust(&ctx).unwrap();
1560 let s1_causes_s2 = results
1562 .iter()
1563 .find(|r| r.cause == "s1" && r.effect == "s2")
1564 .unwrap();
1565 assert!(
1566 s1_causes_s2.significant,
1567 "HC3: s1 should Granger-cause s2: F={:.3}, p={:.4}, adj_p={:.4}",
1568 s1_causes_s2.f_statistic, s1_causes_s2.p_value, s1_causes_s2.adjusted_p_value,
1569 );
1570 }
1571
1572 #[test]
1590 fn f_distribution_sf_matches_the_closed_form_for_d1_eq_2() {
1591 for d2 in [1.0_f64, 2.0, 3.0, 5.0, 10.0, 30.0, 100.0, 500.0] {
1592 for x in [0.01_f64, 0.1, 0.5, 1.0, 2.0, 3.5, 5.0, 10.0, 50.0, 1000.0] {
1593 let expected = (1.0 + 2.0 * x / d2).powf(-d2 / 2.0);
1594 let got = f_distribution_sf(x, 2.0, d2);
1595 assert!(
1596 (got - expected).abs() < 1e-9,
1597 "F(2, {d2}) at {x}: got {got:.12}, closed form {expected:.12}"
1598 );
1599 }
1600 }
1601 }
1602
1603 #[test]
1605 fn f_distribution_sf_matches_the_closed_form_for_d2_eq_2() {
1606 for d1 in [1.0_f64, 2.0, 3.0, 5.0, 10.0, 30.0, 100.0] {
1607 for x in [0.01_f64, 0.1, 0.5, 1.0, 2.0, 3.5, 5.0, 10.0, 50.0, 1000.0] {
1608 let expected = 1.0 - (d1 * x / (2.0 + d1 * x)).powf(d1 / 2.0);
1609 let got = f_distribution_sf(x, d1, 2.0);
1610 assert!(
1611 (got - expected).abs() < 1e-9,
1612 "F({d1}, 2) at {x}: got {got:.12}, closed form {expected:.12}"
1613 );
1614 }
1615 }
1616 }
1617
1618 #[test]
1620 fn f_distribution_sf_matches_the_closed_form_for_one_and_one() {
1621 for x in [0.001_f64, 0.01, 0.1, 0.5, 1.0, 2.0, 7.0, 100.0, 10_000.0] {
1622 let expected = 1.0 - 2.0 / std::f64::consts::PI * x.sqrt().atan();
1623 let got = f_distribution_sf(x, 1.0, 1.0);
1624 assert!(
1625 (got - expected).abs() < 1e-9,
1626 "F(1, 1) at {x}: got {got:.12}, closed form {expected:.12}"
1627 );
1628 }
1629 }
1630
1631 #[test]
1639 fn f_distribution_sf_is_consistent_under_reciprocal() {
1640 for d1 in [1.0_f64, 3.0, 7.0, 12.0, 40.0] {
1641 for d2 in [1.0_f64, 3.0, 7.0, 12.0, 40.0] {
1642 for x in [0.05_f64, 0.3, 1.0, 2.5, 9.0, 60.0] {
1643 let a = f_distribution_sf(x, d1, d2);
1644 let b = 1.0 - f_distribution_sf(1.0 / x, d2, d1);
1645 assert!(
1646 (a - b).abs() < 1e-9,
1647 "F({d1},{d2}) at {x}: {a:.12} vs reciprocal identity {b:.12}"
1648 );
1649 }
1650 }
1651 }
1652 }
1653
1654 #[test]
1656 fn f_distribution_sf_is_a_survival_function() {
1657 for d1 in [1.0_f64, 2.0, 5.0, 17.0] {
1658 for d2 in [1.0_f64, 2.0, 5.0, 17.0] {
1659 let mut previous = f64::INFINITY;
1660 for k in 0..200 {
1661 let x = f64::from(k) * 0.25 + 0.01;
1662 let v = f_distribution_sf(x, d1, d2);
1663 assert!((0.0..=1.0).contains(&v), "F({d1},{d2}) at {x}: {v}");
1664 assert!(v <= previous + 1e-12, "F({d1},{d2}) increased at {x}");
1665 previous = v;
1666 }
1667 }
1668 }
1669 }
1670
1671 #[test]
1672 fn f_distribution_sf_matches_scipy() {
1673 const SCIPY: &[(f64, f64, f64, f64)] = &[
1678 (0.05_f64, 1.0, 1.0, 8.599_513_039_068_979e-1),
1679 (0.5_f64, 1.0, 1.0, 6.081_734_479_693_929e-1),
1680 (1.0_f64, 1.0, 1.0, 5.000_000_000_000_001e-1),
1681 (2.5_f64, 1.0, 1.0, 3.590_170_359_713_762e-1),
1682 (5.0_f64, 1.0, 1.0, 2.677_204_728_012_301e-1),
1683 (20.0_f64, 1.0, 1.0, 1.400_486_960_931_020_5e-1),
1684 (200.0_f64, 1.0, 1.0, 4.494_101_372_651_411e-2),
1685 (0.05_f64, 1.0, 3.0, 8.374_248_972_889_173e-1),
1686 (0.5_f64, 1.0, 3.0, 5.304_777_709_329_569e-1),
1687 (1.0_f64, 1.0, 3.0, 3.910_022_189_557_707e-1),
1688 (2.5_f64, 1.0, 3.0, 2.119_854_426_726_486_8e-1),
1689 (5.0_f64, 1.0, 3.0, 1.113_671_547_140_838_7e-1),
1690 (20.0_f64, 1.0, 3.0, 2.083_515_119_618_485e-2),
1691 (200.0_f64, 1.0, 3.0, 7.658_843_583_400_378e-4),
1692 (0.05_f64, 1.0, 5.0, 8.319_122_479_866_876e-1),
1693 (0.5_f64, 1.0, 5.0, 5.110_840_804_302_803e-1),
1694 (1.0_f64, 1.0, 5.0, 3.632_174_676_491_225_5e-1),
1695 (2.5_f64, 1.0, 5.0, 1.746_878_142_641_194_2e-1),
1696 (5.0_f64, 1.0, 5.0, 7.558_681_842_161_245e-2),
1697 (20.0_f64, 1.0, 5.0, 6.566_271_827_563_008e-3),
1698 (200.0_f64, 1.0, 5.0, 3.182_292_826_705_155e-5),
1699 (0.05_f64, 1.0, 7.0, 8.294_489_446_380_614e-1),
1700 (0.5_f64, 1.0, 7.0, 5.023_540_170_799_178e-1),
1701 (1.0_f64, 1.0, 7.0, 3.506_166_628_202_073_7e-1),
1702 (2.5_f64, 1.0, 7.0, 1.578_593_227_958_298_7e-1),
1703 (5.0_f64, 1.0, 7.0, 6.042_742_018_442_884e-2),
1704 (20.0_f64, 1.0, 7.0, 2.893_495_120_607_208e-3),
1705 (200.0_f64, 1.0, 7.0, 2.097_359_711_842_887e-6),
1706 (0.05_f64, 1.0, 10.0, 8.275_651_592_009_66e-1),
1707 (0.5_f64, 1.0, 10.0, 4.956_475_043_831_195_5e-1),
1708 (1.0_f64, 1.0, 10.0, 3.408_931_323_020_597_5e-1),
1709 (2.5_f64, 1.0, 10.0, 1.449_276_054_040_804_8e-1),
1710 (5.0_f64, 1.0, 10.0, 4.933_219_563_992_176_4e-2),
1711 (20.0_f64, 1.0, 10.0, 1.193_466_830_020_324_4e-3),
1712 (200.0_f64, 1.0, 10.0, 6.149_001_368_036_42e-8),
1713 (0.05_f64, 1.0, 30.0, 8.245_790_072_177_894e-1),
1714 (0.5_f64, 1.0, 30.0, 4.849_569_686_830_377e-1),
1715 (1.0_f64, 1.0, 30.0, 3.253_086_154_260_302_3e-1),
1716 (2.5_f64, 1.0, 30.0, 1.243_334_200_465_653_3e-1),
1717 (5.0_f64, 1.0, 30.0, 3.293_630_592_563_78e-2),
1718 (20.0_f64, 1.0, 30.0, 1.029_210_178_637_816_9e-4),
1719 (200.0_f64, 1.0, 30.0, 8.298_453_462_587_944e-15),
1720 (0.05_f64, 1.0, 100.0, 8.235_194_441_364_071e-1),
1721 (0.5_f64, 1.0, 100.0, 4.811_446_769_857_403_7e-1),
1722 (1.0_f64, 1.0, 100.0, 3.197_241_557_841_232e-1),
1723 (2.5_f64, 1.0, 100.0, 1.170_041_896_091_983_8e-1),
1724 (5.0_f64, 1.0, 100.0, 2.756_960_066_604_534_4e-2),
1725 (20.0_f64, 1.0, 100.0, 2.049_634_555_794_899_3e-5),
1726 (200.0_f64, 1.0, 100.0, 1.351_242_379_602_169_4e-25),
1727 (0.05_f64, 1.0, 500.0, 8.231_546_038_971_818e-1),
1728 (0.5_f64, 1.0, 500.0, 4.798_295_396_595_345_7e-1),
1729 (1.0_f64, 1.0, 500.0, 3.177_942_072_606_256e-1),
1730 (2.5_f64, 1.0, 500.0, 1.144_786_392_733_747_5e-1),
1731 (5.0_f64, 1.0, 500.0, 2.578_771_199_599_521_8e-2),
1732 (20.0_f64, 1.0, 500.0, 9.592_901_087_300_635e-6),
1733 (200.0_f64, 1.0, 500.0, 1.950_412_009_179_663e-38),
1734 (0.05_f64, 3.0, 1.0, 9.791_648_488_038_152e-1),
1735 (0.5_f64, 3.0, 1.0, 7.477_845_036_444_957e-1),
1736 (1.0_f64, 3.0, 1.0, 6.089_977_810_442_295e-1),
1737 (2.5_f64, 3.0, 1.0, 4.279_966_192_993_59e-1),
1738 (5.0_f64, 3.0, 1.0, 3.149_623_575_257_075e-1),
1739 (20.0_f64, 3.0, 1.0, 1.625_751_027_110_831_4e-1),
1740 (200.0_f64, 3.0, 1.0, 5.192_211_792_598_956e-2),
1741 (0.05_f64, 3.0, 3.0, 9.826_133_295_298_128e-1),
1742 (0.5_f64, 3.0, 3.0, 7.082_085_942_090_715e-1),
1743 (1.0_f64, 3.0, 3.0, 5.000_000_000_000_001e-1),
1744 (2.5_f64, 3.0, 3.0, 2.357_618_226_542_65e-1),
1745 (5.0_f64, 3.0, 3.0, 1.095_510_187_085_240_3e-1),
1746 (20.0_f64, 3.0, 3.0, 1.738_667_047_018_715_6e-2),
1747 (200.0_f64, 3.0, 3.0, 5.948_473_159_638_333e-4),
1748 (0.05_f64, 3.0, 5.0, 9.835_627_319_613_537e-1),
1749 (0.5_f64, 3.0, 5.0, 6.984_526_373_049_242e-1),
1750 (1.0_f64, 3.0, 5.0, 4.648_547_899_936_353_3e-1),
1751 (2.5_f64, 3.0, 5.0, 1.739_276_579_365_1e-1),
1752 (5.0_f64, 3.0, 5.0, 5.766_888_562_243_733e-2),
1753 (20.0_f64, 3.0, 5.0, 3.250_005_065_191_810_5e-3),
1754 (200.0_f64, 3.0, 5.0, 1.261_190_950_980_022_2e-5),
1755 (0.05_f64, 3.0, 7.0, 9.840_063_156_479_888e-1),
1756 (0.5_f64, 3.0, 7.0, 6.940_363_875_688_136e-1),
1757 (1.0_f64, 3.0, 7.0, 4.470_796_134_684_835e-1),
1758 (2.5_f64, 3.0, 7.0, 1.435_094_562_789_392_7e-1),
1759 (5.0_f64, 3.0, 7.0, 3.667_335_421_818_645e-2),
1760 (20.0_f64, 3.0, 7.0, 8.225_792_466_075_144e-4),
1761 (200.0_f64, 3.0, 7.0, 3.817_227_678_744_054_4e-7),
1762 (0.05_f64, 3.0, 10.0, 9.843_549_357_040_79e-1),
1763 (0.5_f64, 3.0, 10.0, 6.906_222_455_335_574e-1),
1764 (1.0_f64, 3.0, 10.0, 4.323_372_030_216_968e-1),
1765 (2.5_f64, 3.0, 10.0, 1.190_395_626_582_781_6e-1),
1766 (5.0_f64, 3.0, 10.0, 2.261_392_275_109_628_4e-2),
1767 (20.0_f64, 3.0, 10.0, 1.511_650_265_860_084_4e-4),
1768 (200.0_f64, 3.0, 10.0, 3.183_148_174_576_168_7e-9),
1769 (0.05_f64, 3.0, 30.0, 9.849_263_153_922_542e-1),
1770 (0.5_f64, 3.0, 30.0, 6.851_195_412_952_075e-1),
1771 (1.0_f64, 3.0, 30.0, 4.063_572_668_729_493e-1),
1772 (2.5_f64, 3.0, 30.0, 7.847_395_791_463_87e-2),
1773 (5.0_f64, 3.0, 30.0, 6.254_870_601_873_435e-3),
1774 (20.0_f64, 3.0, 30.0, 2.587_600_271_284_227e-7),
1775 (200.0_f64, 3.0, 30.0, 6.425_629_332_909_392e-20),
1776 (0.05_f64, 3.0, 100.0, 9.851_351_172_793_991e-1),
1777 (0.5_f64, 3.0, 100.0, 6.831_325_288_938_818e-1),
1778 (1.0_f64, 3.0, 100.0, 3.961_862_496_180_044e-1),
1779 (2.5_f64, 3.0, 100.0, 6.383_295_997_908_654e-2),
1780 (5.0_f64, 3.0, 100.0, 2.844_573_100_886_653e-3),
1781 (20.0_f64, 3.0, 100.0, 3.112_606_226_774_384_6e-10),
1782 (200.0_f64, 3.0, 100.0, 4.144_844_518_612_426e-42),
1783 (0.05_f64, 3.0, 500.0, 9.852_077_993_840_09e-1),
1784 (0.5_f64, 3.0, 500.0, 6.824_432_968_133_542e-1),
1785 (1.0_f64, 3.0, 500.0, 3.925_476_468_585_115_7e-1),
1786 (2.5_f64, 3.0, 500.0, 5.881_163_820_983_985e-2),
1787 (5.0_f64, 3.0, 500.0, 2.001_302_671_949_856e-3),
1788 (20.0_f64, 3.0, 500.0, 2.948_423_715_311_517_2e-12),
1789 (200.0_f64, 3.0, 500.0, 3.277_206_998_069_135e-85),
1790 (0.05_f64, 5.0, 1.0, 9.934_337_281_724_37e-1),
1791 (0.5_f64, 5.0, 1.0, 7.835_627_707_303_144e-1),
1792 (1.0_f64, 5.0, 1.0, 6.367_825_323_508_775e-1),
1793 (2.5_f64, 5.0, 1.0, 4.451_217_169_436_3e-1),
1794 (5.0_f64, 5.0, 1.0, 3.265_715_644_624_462_3e-1),
1795 (20.0_f64, 5.0, 1.0, 1.680_877_520_133_123e-1),
1796 (200.0_f64, 5.0, 1.0, 5.363_087_276_333_012e-2),
1797 (0.05_f64, 5.0, 3.0, 9.967_499_949_348_082e-1),
1798 (0.5_f64, 5.0, 3.0, 7.673_760_819_999_214e-1),
1799 (1.0_f64, 5.0, 3.0, 5.351_452_100_063_651e-1),
1800 (2.5_f64, 5.0, 3.0, 2.405_360_338_406_623_6e-1),
1801 (5.0_f64, 5.0, 3.0, 1.078_163_782_679_175_8e-1),
1802 (20.0_f64, 5.0, 3.0, 1.643_726_803_864_62e-2),
1803 (200.0_f64, 5.0, 3.0, 5.539_101_145_966_495e-4),
1804 (0.05_f64, 5.0, 5.0, 9.974_477_392_801_275e-1),
1805 (0.5_f64, 5.0, 5.0, 7.674_886_808_696_213e-1),
1806 (1.0_f64, 5.0, 5.0, 5.000_000_000_000_001e-1),
1807 (2.5_f64, 5.0, 5.0, 1.686_841_555_429_12e-1),
1808 (5.0_f64, 5.0, 5.0, 5.096_973_941_492_917_4e-2),
1809 (20.0_f64, 5.0, 5.0, 2.552_260_719_872_432_3e-3),
1810 (200.0_f64, 5.0, 5.0, 9.433_866_982_533_933e-6),
1811 (0.05_f64, 5.0, 7.0, 9.977_457_456_693_496e-1),
1812 (0.5_f64, 5.0, 7.0, 7.685_843_297_279_157e-1),
1813 (1.0_f64, 5.0, 7.0, 4.812_939_129_582_055e-1),
1814 (2.5_f64, 5.0, 7.0, 1.320_062_236_078_406_7e-1),
1815 (5.0_f64, 5.0, 7.0, 2.874_617_655_405_799e-2),
1816 (20.0_f64, 5.0, 7.0, 5.139_310_327_531_792e-4),
1817 (200.0_f64, 5.0, 7.0, 2.155_804_614_637_414e-7),
1818 (0.05_f64, 5.0, 10.0, 9.979_679_819_314_153e-1),
1819 (0.5_f64, 5.0, 10.0, 7.700_248_806_501_017e-1),
1820 (1.0_f64, 5.0, 10.0, 4.651_194_265_378_001_4e-1),
1821 (2.5_f64, 5.0, 10.0, 1.020_022_766_442_697_7e-1),
1822 (5.0_f64, 5.0, 10.0, 1.486_880_040_811_298_2e-2),
1823 (20.0_f64, 5.0, 10.0, 6.472_349_610_750_509e-5),
1824 (200.0_f64, 5.0, 10.0, 1.102_329_910_570_809e-9),
1825 (0.05_f64, 5.0, 30.0, 9.983_102_236_198_341e-1),
1826 (0.5_f64, 5.0, 30.0, 7.737_335_937_035_952e-1),
1827 (1.0_f64, 5.0, 30.0, 4.346_488_763_398_733e-1),
1828 (2.5_f64, 5.0, 30.0, 5.244_410_472_445_027_6e-2),
1829 (5.0_f64, 5.0, 30.0, 1.896_967_469_088_041_7e-3),
1830 (20.0_f64, 5.0, 30.0, 9.581_174_502_890_943e-9),
1831 (200.0_f64, 5.0, 30.0, 4.352_520_121_551_445e-22),
1832 (0.05_f64, 5.0, 100.0, 9.984_286_736_809_271e-1),
1833 (0.5_f64, 5.0, 100.0, 7.755_895_192_354_815e-1),
1834 (1.0_f64, 5.0, 100.0, 4.218_298_943_719_714_4e-1),
1835 (2.5_f64, 5.0, 100.0, 3.544_824_945_021_782e-2),
1836 (5.0_f64, 5.0, 100.0, 3.953_080_102_324_142_3e-4),
1837 (20.0_f64, 5.0, 100.0, 8.923_890_603_183_405e-14),
1838 (200.0_f64, 5.0, 100.0, 2.043_831_493_329_358_6e-50),
1839 (0.05_f64, 5.0, 500.0, 9.984_690_929_254_346e-1),
1840 (0.5_f64, 5.0, 500.0, 7.763_082_744_903_42e-1),
1841 (1.0_f64, 5.0, 500.0, 4.170_943_401_351_524e-1),
1842 (2.5_f64, 5.0, 500.0, 2.989_775_590_717_396_3e-2),
1843 (5.0_f64, 5.0, 500.0, 1.764_704_885_541_849_8e-4),
1844 (20.0_f64, 5.0, 500.0, 3.364_299_050_353_227e-18),
1845 (200.0_f64, 5.0, 500.0, 8.577_660_241_290_851e-117),
1846 (0.05_f64, 7.0, 1.0, 9.971_065_048_793_928e-1),
1847 (0.5_f64, 7.0, 1.0, 7.997_999_258_337_599e-1),
1848 (1.0_f64, 7.0, 1.0, 6.493_833_371_797_926e-1),
1849 (2.5_f64, 7.0, 1.0, 4.528_161_585_563_629e-1),
1850 (5.0_f64, 7.0, 1.0, 3.317_689_599_284_96e-1),
1851 (20.0_f64, 7.0, 1.0, 1.705_510_553_619_384_6e-1),
1852 (200.0_f64, 7.0, 1.0, 5.439_421_527_116_384e-2),
1853 (0.05_f64, 7.0, 3.0, 9.991_774_207_533_924e-1),
1854 (0.5_f64, 7.0, 3.0, 7.973_063_575_133_491e-1),
1855 (1.0_f64, 7.0, 3.0, 5.529_203_865_315_162e-1),
1856 (2.5_f64, 7.0, 3.0, 2.424_975_282_447_001e-1),
1857 (5.0_f64, 7.0, 3.0, 1.068_320_443_375_101_9e-1),
1858 (20.0_f64, 7.0, 3.0, 1.599_368_435_201_107_8e-2),
1859 (200.0_f64, 7.0, 3.0, 5.354_274_210_490_03e-4),
1860 (0.05_f64, 7.0, 5.0, 9.994_860_689_672_468e-1),
1861 (0.5_f64, 7.0, 5.0, 8.043_267_551_331_769e-1),
1862 (1.0_f64, 7.0, 5.0, 5.187_060_870_417_941e-1),
1863 (2.5_f64, 7.0, 5.0, 1.651_665_724_962_968_2e-1),
1864 (5.0_f64, 7.0, 5.0, 4.758_354_977_857_251_5e-2),
1865 (20.0_f64, 7.0, 5.0, 2.254_254_330_650_468e-3),
1866 (200.0_f64, 7.0, 5.0, 8.156_429_888_424_49e-6),
1867 (0.05_f64, 7.0, 7.0, 9.996_005_787_903_773e-1),
1868 (0.5_f64, 7.0, 7.0, 8.096_434_091_615_682e-1),
1869 (1.0_f64, 7.0, 7.0, 4.999_999_999_999_998_3e-1),
1870 (2.5_f64, 7.0, 7.0, 1.248_783_321_232_528_3e-1),
1871 (5.0_f64, 7.0, 7.0, 2.493_361_528_444_257_3e-2),
1872 (20.0_f64, 7.0, 7.0, 3.994_212_096_226_762_7e-4),
1873 (200.0_f64, 7.0, 7.0, 1.602_201_087_428_974_8e-7),
1874 (0.05_f64, 7.0, 10.0, 9.996_785_729_851_23e-1),
1875 (0.5_f64, 7.0, 10.0, 8.150_431_465_936_292e-1),
1876 (1.0_f64, 7.0, 10.0, 4.834_025_108_823_465e-1),
1877 (2.5_f64, 7.0, 10.0, 9.167_374_844_440_98e-2),
1878 (5.0_f64, 7.0, 10.0, 1.146_120_704_541_107_4e-2),
1879 (20.0_f64, 7.0, 10.0, 4.017_926_806_906_255e-5),
1880 (200.0_f64, 7.0, 10.0, 6.221_655_538_495_649e-10),
1881 (0.05_f64, 7.0, 30.0, 9.997_853_615_351_379e-1),
1882 (0.5_f64, 7.0, 30.0, 8.269_609_583_211_951e-1),
1883 (1.0_f64, 7.0, 30.0, 4.505_706_879_613_486e-1),
1884 (2.5_f64, 7.0, 30.0, 3.777_766_128_993_187_4e-2),
1885 (5.0_f64, 7.0, 30.0, 7.711_805_851_010_107e-4),
1886 (20.0_f64, 7.0, 30.0, 1.100_187_450_229_647_2e-9),
1887 (200.0_f64, 7.0, 30.0, 2.202_044_443_436_217_8e-23),
1888 (0.05_f64, 7.0, 100.0, 9.998_183_096_039_113e-1),
1889 (0.5_f64, 7.0, 100.0, 8.325_416_801_853_718e-1),
1890 (1.0_f64, 7.0, 100.0, 4.359_080_275_908_213_7e-1),
1891 (2.5_f64, 7.0, 100.0, 2.075_541_485_050_993e-2),
1892 (5.0_f64, 7.0, 100.0, 6.953_028_285_653_315e-5),
1893 (20.0_f64, 7.0, 100.0, 1.522_620_759_402_403_3e-16),
1894 (200.0_f64, 7.0, 100.0, 7.676_461_532_132_527e-56),
1895 (0.05_f64, 7.0, 500.0, 9.998_290_563_503_065e-1),
1896 (0.5_f64, 7.0, 500.0, 8.346_733_652_780_742e-1),
1897 (1.0_f64, 7.0, 500.0, 4.303_267_111_651_259e-1),
1898 (2.5_f64, 7.0, 500.0, 1.564_022_931_445_676_8e-2),
1899 (5.0_f64, 7.0, 500.0, 1.724_493_926_108_725_2e-5),
1900 (20.0_f64, 7.0, 500.0, 1.105_913_154_993_826_2e-23),
1901 (200.0_f64, 7.0, 500.0, 1.602_972_532_619_707_2e-140),
1902 (0.05_f64, 10.0, 1.0, 9.988_065_331_699_797e-1),
1903 (0.5_f64, 10.0, 1.0, 8.123_301_291_303_971e-1),
1904 (1.0_f64, 10.0, 1.0, 6.591_068_676_979_402e-1),
1905 (2.5_f64, 10.0, 1.0, 4.587_204_829_251_881e-1),
1906 (5.0_f64, 10.0, 1.0, 3.357_485_282_688_647_5e-1),
1907 (20.0_f64, 10.0, 1.0, 1.724_348_407_990_325e-1),
1908 (200.0_f64, 10.0, 1.0, 5.497_784_197_229_169e-2),
1909 (0.05_f64, 10.0, 3.0, 9.998_488_349_734_14e-1),
1910 (0.5_f64, 10.0, 3.0, 8.219_925_926_248_246e-1),
1911 (1.0_f64, 10.0, 3.0, 5.676_627_969_783_027e-1),
1912 (2.5_f64, 10.0, 3.0, 2.439_174_274_603_792_3e-1),
1913 (5.0_f64, 10.0, 3.0, 1.059_818_885_792_791_2e-1),
1914 (20.0_f64, 10.0, 3.0, 1.564_506_429_592_112_5e-2),
1915 (200.0_f64, 10.0, 3.0, 5.211_599_742_799_054e-4),
1916 (0.05_f64, 10.0, 5.0, 9.999_352_765_038_925e-1),
1917 (0.5_f64, 10.0, 5.0, 8.358_050_491_002_612e-1),
1918 (1.0_f64, 10.0, 5.0, 5.348_805_734_621_997e-1),
1919 (2.5_f64, 10.0, 5.0, 1.618_347_415_219_576_2e-1),
1920 (5.0_f64, 10.0, 5.0, 4.480_822_975_357_040_5e-2),
1921 (20.0_f64, 10.0, 5.0, 2.032_018_068_584_613e-3),
1922 (200.0_f64, 10.0, 5.0, 7.234_158_868_087_465e-6),
1923 (0.05_f64, 10.0, 7.0, 9.999_598_207_319_309e-1),
1924 (0.5_f64, 10.0, 7.0, 8.454_966_997_285_922e-1),
1925 (1.0_f64, 10.0, 7.0, 5.165_974_891_176_535e-1),
1926 (2.5_f64, 10.0, 7.0, 1.183_060_618_644_493_1e-1),
1927 (5.0_f64, 10.0, 7.0, 2.191_557_852_489_411_6e-2),
1928 (20.0_f64, 10.0, 7.0, 3.214_270_148_770_766e-4),
1929 (200.0_f64, 10.0, 7.0, 1.246_086_026_245_263_5e-7),
1930 (0.05_f64, 10.0, 10.0, 9.999_737_541_172_163e-1),
1931 (0.5_f64, 10.0, 10.0, 8.551_541_939_744_957e-1),
1932 (1.0_f64, 10.0, 10.0, 5.000_000_000_000_001e-1),
1933 (2.5_f64, 10.0, 10.0, 8.225_366_322_272_007e-2),
1934 (5.0_f64, 10.0, 10.0, 8.950_061_601_381_901e-3),
1935 (20.0_f64, 10.0, 10.0, 2.624_588_278_370_414_6e-5),
1936 (200.0_f64, 10.0, 10.0, 3.777_237_977_079_334e-10),
1937 (0.05_f64, 10.0, 30.0, 9.999_886_383_739_246e-1),
1938 (0.5_f64, 10.0, 30.0, 8.763_612_630_739_956e-1),
1939 (1.0_f64, 10.0, 30.0, 4.654_242_904_944_112e-1),
1940 (2.5_f64, 10.0, 30.0, 2.556_200_682_455_132e-2),
1941 (5.0_f64, 10.0, 30.0, 2.814_713_926_752_866e-4),
1942 (20.0_f64, 10.0, 30.0, 1.238_523_030_227_409_6e-10),
1943 (200.0_f64, 10.0, 30.0, 1.283_889_691_329_897_6e-24),
1944 (0.05_f64, 10.0, 100.0, 9.999_921_314_059_984e-1),
1945 (0.5_f64, 10.0, 100.0, 8.863_503_911_530_737e-1),
1946 (1.0_f64, 10.0, 100.0, 4.488_172_795_604_992e-1),
1947 (2.5_f64, 10.0, 100.0, 1.009_520_838_004_780_5e-2),
1948 (5.0_f64, 10.0, 100.0, 7.203_294_285_246_187e-6),
1949 (20.0_f64, 10.0, 100.0, 9.052_974_736_110_621e-20),
1950 (200.0_f64, 10.0, 100.0, 2.023_087_478_925_676_3e-61),
1951 (0.05_f64, 10.0, 500.0, 9.999_931_479_117_107e-1),
1952 (0.5_f64, 10.0, 500.0, 8.901_834_974_380_145e-1),
1953 (1.0_f64, 10.0, 500.0, 4.422_289_394_076_182_6e-1),
1954 (2.5_f64, 10.0, 500.0, 6.181_310_892_451_748e-3),
1955 (5.0_f64, 10.0, 500.0, 6.122_346_115_150_638e-7),
1956 (20.0_f64, 10.0, 500.0, 3.451_499_099_408_449_6e-31),
1957 (200.0_f64, 10.0, 500.0, 1.260_108_905_566_712_7e-167),
1958 (0.05_f64, 20.0, 1.0, 9.997_665_525_652_514e-1),
1959 (0.5_f64, 20.0, 1.0, 8.273_216_960_128_282e-1),
1960 (1.0_f64, 20.0, 1.0, 6.707_434_228_282_909e-1),
1961 (2.5_f64, 20.0, 1.0, 4.657_462_675_288_914e-1),
1962 (5.0_f64, 20.0, 1.0, 3.404_734_867_967_272_3e-1),
1963 (20.0_f64, 20.0, 1.0, 1.746_685_268_748_851_8e-1),
1964 (200.0_f64, 20.0, 1.0, 5.566_968_855_192_703e-2),
1965 (0.05_f64, 20.0, 3.0, 9.999_968_983_502_592e-1),
1966 (0.5_f64, 20.0, 3.0, 8.535_611_969_133_787e-1),
1967 (1.0_f64, 20.0, 3.0, 5.867_480_859_375_398e-1),
1968 (2.5_f64, 20.0, 3.0, 2.455_033_268_956_120_8e-1),
1969 (5.0_f64, 20.0, 3.0, 1.048_551_121_300_250_3e-1),
1970 (20.0_f64, 20.0, 3.0, 1.521_997_452_120_014_3e-2),
1971 (200.0_f64, 20.0, 3.0, 5.040_490_534_002_49e-4),
1972 (0.05_f64, 20.0, 5.0, 9.999_996_332_912_71e-1),
1973 (0.5_f64, 20.0, 5.0, 8.774_927_553_181_574e-1),
1974 (1.0_f64, 20.0, 5.0, 5.569_748_153_151_206e-1),
1975 (2.5_f64, 20.0, 5.0, 1.569_969_392_108_339e-1),
1976 (5.0_f64, 20.0, 5.0, 4.128_835_849_042_164e-2),
1977 (20.0_f64, 20.0, 5.0, 1.774_850_437_874_341_7e-3),
1978 (200.0_f64, 20.0, 5.0, 6.198_937_506_367_039e-6),
1979 (0.05_f64, 20.0, 7.0, 9.999_999_022_681_04e-1),
1980 (0.5_f64, 20.0, 7.0, 8.939_292_004_628_931e-1),
1981 (1.0_f64, 20.0, 7.0, 5.401_531_821_063_319e-1),
1982 (2.5_f64, 20.0, 7.0, 1.089_237_236_633_195_4e-1),
1983 (5.0_f64, 20.0, 7.0, 1.823_902_806_509_197_8e-2),
1984 (20.0_f64, 20.0, 7.0, 2.396_075_955_539_662_8e-4),
1985 (200.0_f64, 20.0, 7.0, 8.920_495_843_834_883e-8),
1986 (0.05_f64, 20.0, 10.0, 9.999_999_726_576_813e-1),
1987 (0.5_f64, 20.0, 10.0, 9.102_172_851_562_5e-1),
1988 (1.0_f64, 20.0, 10.0, 5.244_995_315_671_086e-1),
1989 (2.5_f64, 20.0, 10.0, 6.897_514_678_237_858e-2),
1990 (5.0_f64, 20.0, 10.0, 6.161_513_123_423_422e-3),
1991 (20.0_f64, 20.0, 10.0, 1.437_064_754_200_605_2e-5),
1992 (200.0_f64, 20.0, 10.0, 1.895_016_442_662_043_3e-10),
1993 (0.05_f64, 20.0, 30.0, 9.999_999_984_214_671e-1),
1994 (0.5_f64, 20.0, 30.0, 9.453_350_776_055_7e-1),
1995 (1.0_f64, 20.0, 30.0, 4.890_801_931_489_531e-1),
1996 (2.5_f64, 20.0, 30.0, 1.132_846_370_347_176_2e-2),
1997 (5.0_f64, 20.0, 30.0, 4.132_575_555_705_573e-5),
1998 (20.0_f64, 20.0, 30.0, 3.214_720_610_769_998e-12),
1999 (200.0_f64, 20.0, 30.0, 1.466_612_714_077_889e-26),
2000 (0.05_f64, 20.0, 100.0, 9.999_999_996_344_979e-1),
2001 (0.5_f64, 20.0, 100.0, 9.610_073_391_512_228e-1),
2002 (1.0_f64, 20.0, 100.0, 4.692_089_598_227_433e-1),
2003 (2.5_f64, 20.0, 100.0, 1.499_776_663_571_476_4e-3),
2004 (5.0_f64, 20.0, 100.0, 2.632_416_051_112_641_8e-8),
2005 (20.0_f64, 20.0, 100.0, 1.985_975_732_417_538e-26),
2006 (200.0_f64, 20.0, 100.0, 2.319_515_191_433_788e-71),
2007 (0.05_f64, 20.0, 500.0, 9.999_999_997_993_518e-1),
2008 (0.5_f64, 20.0, 500.0, 9.667_241_763_563_634e-1),
2009 (1.0_f64, 20.0, 500.0, 4.603_768_778_460_684_6e-1),
2010 (2.5_f64, 20.0, 500.0, 3.614_471_679_365_32e-4),
2011 (5.0_f64, 20.0, 500.0, 2.419_896_115_024_529_5e-11),
2012 (20.0_f64, 20.0, 500.0, 1.351_873_364_707_244e-51),
2013 (200.0_f64, 20.0, 500.0, 1.201_843_864_451_444_3e-223),
2014 (0.05_f64, 50.0, 1.0, 9.999_553_080_801_258e-1),
2015 (0.5_f64, 50.0, 1.0, 8.365_038_281_044_941e-1),
2016 (1.0_f64, 50.0, 1.0, 6.778_743_548_997_557e-1),
2017 (2.5_f64, 50.0, 1.0, 4.700_290_002_323_09e-1),
2018 (5.0_f64, 50.0, 1.0, 3.433_477_708_108_625_4e-1),
2019 (20.0_f64, 50.0, 1.0, 1.760_256_107_265_665_6e-1),
2020 (200.0_f64, 50.0, 1.0, 5.608_990_729_137_773e-2),
2021 (0.05_f64, 50.0, 3.0, 9.999_999_881_840_26e-1),
2022 (0.5_f64, 50.0, 3.0, 8.740_579_417_973_806e-1),
2023 (1.0_f64, 50.0, 3.0, 5.993_767_746_957_155e-1),
2024 (2.5_f64, 50.0, 3.0, 2.464_112_141_782_734e-1),
2025 (5.0_f64, 50.0, 3.0, 1.041_029_960_001_185_8e-1),
2026 (20.0_f64, 50.0, 3.0, 1.495_494_664_972_292_7e-2),
2027 (200.0_f64, 50.0, 3.0, 4.935_306_370_114_788e-4),
2028 (0.05_f64, 50.0, 5.0, 9.999_999_999_331_19e-1),
2029 (0.5_f64, 50.0, 5.0, 9.052_470_979_699_002e-1),
2030 (1.0_f64, 50.0, 5.0, 5.725_055_335_833_61e-1),
2031 (2.5_f64, 50.0, 5.0, 1.534_912_727_528_461_3e-1),
2032 (5.0_f64, 50.0, 5.0, 3.901_818_940_991_427e-2),
2033 (20.0_f64, 50.0, 5.0, 1.621_991_784_701_261_1e-3),
2034 (200.0_f64, 50.0, 5.0, 5.599_778_288_513_24e-6),
2035 (0.05_f64, 50.0, 7.0, 9.999_999_999_982_813e-1),
2036 (0.5_f64, 50.0, 7.0, 9.263_825_817_270_023e-1),
2037 (1.0_f64, 50.0, 7.0, 5.575_369_842_173_838e-1),
2038 (2.5_f64, 50.0, 7.0, 1.021_853_712_134_265_7e-1),
2039 (5.0_f64, 50.0, 7.0, 1.596_566_743_101_228_8e-2),
2040 (20.0_f64, 50.0, 7.0, 1.955_206_109_766_565e-4),
2041 (200.0_f64, 50.0, 7.0, 7.102_024_313_914_767e-8),
2042 (0.05_f64, 50.0, 10.0, 9.999_999_999_999_66e-1),
2043 (0.5_f64, 50.0, 10.0, 9.468_123_117_221_907e-1),
2044 (1.0_f64, 50.0, 10.0, 5.436_430_945_196_307e-1),
2045 (2.5_f64, 50.0, 10.0, 5.958_672_931_842_764e-2),
2046 (5.0_f64, 50.0, 10.0, 4.615_101_165_139_315e-3),
2047 (20.0_f64, 50.0, 10.0, 9.266_790_041_201_036e-6),
2048 (200.0_f64, 50.0, 10.0, 1.158_252_031_677_620_7e-10),
2049 (0.05_f64, 50.0, 30.0, 9.999_999_999_999_999e-1),
2050 (0.5_f64, 50.0, 30.0, 9.852_667_345_044_902e-1),
2051 (1.0_f64, 50.0, 30.0, 5.108_974_983_279_239e-1),
2052 (2.5_f64, 50.0, 30.0, 4.393_558_041_015_697_4e-3),
2053 (5.0_f64, 50.0, 30.0, 5.660_692_594_295_936e-6),
2054 (20.0_f64, 50.0, 30.0, 1.192_477_741_574_441_5e-13),
2055 (200.0_f64, 50.0, 30.0, 3.224_186_055_438_517_6e-28),
2056 (0.05_f64, 50.0, 100.0, 9.999_999_999_999_999e-1),
2057 (0.5_f64, 50.0, 100.0, 9.961_734_860_330_544e-1),
2058 (1.0_f64, 50.0, 100.0, 4.891_205_078_079_293e-1),
2059 (2.5_f64, 50.0, 100.0, 5.068_369_209_338_273_5e-5),
2060 (5.0_f64, 50.0, 100.0, 4.192_942_714_416_357e-12),
2061 (20.0_f64, 50.0, 100.0, 1.590_676_925_542_226_2e-34),
2062 (200.0_f64, 50.0, 100.0, 8.434_051_290_315_383e-82),
2063 (0.05_f64, 50.0, 500.0, 9.999_999_999_999_999e-1),
2064 (0.5_f64, 50.0, 500.0, 9.984_090_605_193_361e-1),
2065 (1.0_f64, 50.0, 500.0, 4.771_617_689_801_394e-1),
2066 (2.5_f64, 50.0, 500.0, 2.900_216_183_887_641_3e-7),
2067 (5.0_f64, 50.0, 500.0, 7.588_419_680_838_374e-22),
2068 (20.0_f64, 50.0, 500.0, 5.990_923_934_343_106e-90),
2069 (200.0_f64, 50.0, 500.0, 1.590_323_134_507_893_6e-297),
2070 ];
2071 for &(f, d1, d2, expected) in SCIPY {
2072 let got = f_distribution_sf(f, d1, d2);
2073 let tol = 1e-11_f64.max(expected.abs() * 1e-9);
2074 assert!(
2075 (got - expected).abs() < tol,
2076 "F({d1},{d2}) sf({f}): got {got:.17e}, scipy {expected:.17e}"
2077 );
2078 }
2079 }
2080}