1use crate::matrix::FdMatrix;
24use crate::maybe_par_chunks_mut_enumerate;
25use rand::prelude::*;
26use rand_distr::Normal;
27use std::f64::consts::PI;
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31#[non_exhaustive]
32pub enum EFunType {
33 Fourier = 0,
35 Poly = 1,
37 PolyHigh = 2,
39 Wiener = 3,
41}
42
43impl EFunType {
44 pub fn from_i32(value: i32) -> Result<Self, crate::FdarError> {
46 match value {
47 0 => Ok(EFunType::Fourier),
48 1 => Ok(EFunType::Poly),
49 2 => Ok(EFunType::PolyHigh),
50 3 => Ok(EFunType::Wiener),
51 _ => Err(crate::FdarError::InvalidEnumValue {
52 enum_name: "EFunType",
53 value,
54 }),
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
61#[non_exhaustive]
62pub enum EValType {
63 Linear = 0,
65 Exponential = 1,
67 Wiener = 2,
69}
70
71impl EValType {
72 pub fn from_i32(value: i32) -> Result<Self, crate::FdarError> {
74 match value {
75 0 => Ok(EValType::Linear),
76 1 => Ok(EValType::Exponential),
77 2 => Ok(EValType::Wiener),
78 _ => Err(crate::FdarError::InvalidEnumValue {
79 enum_name: "EValType",
80 value,
81 }),
82 }
83 }
84}
85
86pub fn fourier_eigenfunctions(t: &[f64], m: usize) -> FdMatrix {
104 let n = t.len();
105 let mut phi = FdMatrix::zeros(n, m);
106 let sqrt2 = 2.0_f64.sqrt();
107
108 for (i, &ti) in t.iter().enumerate() {
109 phi[(i, 0)] = 1.0;
111
112 let mut k = 1; let mut freq = 1; while k < m {
116 if k < m {
118 phi[(i, k)] = sqrt2 * (2.0 * PI * f64::from(freq) * ti).sin();
119 k += 1;
120 }
121 if k < m {
123 phi[(i, k)] = sqrt2 * (2.0 * PI * f64::from(freq) * ti).cos();
124 k += 1;
125 }
126 freq += 1;
127 }
128 }
129 phi
130}
131
132pub fn legendre_eigenfunctions(t: &[f64], m: usize, high: bool) -> FdMatrix {
145 let n = t.len();
146 let mut phi = FdMatrix::zeros(n, m);
147 let start_deg = if high { 2 } else { 0 };
148
149 for (i, &ti) in t.iter().enumerate() {
150 let x = 2.0 * ti - 1.0;
152
153 for j in 0..m {
154 let deg = start_deg + j;
155 let p = legendre_p(x, deg);
157 let norm = ((2 * deg + 1) as f64).sqrt();
159 phi[(i, j)] = p * norm;
160 }
161 }
162 phi
163}
164
165fn legendre_p(x: f64, n: usize) -> f64 {
170 if n == 0 {
171 return 1.0;
172 }
173 if n == 1 {
174 return x;
175 }
176
177 let mut p_prev = 1.0;
178 let mut p_curr = x;
179
180 for k in 2..=n {
181 let p_next = ((2 * k - 1) as f64 * x * p_curr - (k - 1) as f64 * p_prev) / k as f64;
182 p_prev = p_curr;
183 p_curr = p_next;
184 }
185 p_curr
186}
187
188pub fn wiener_eigenfunctions(t: &[f64], m: usize) -> FdMatrix {
202 let n = t.len();
203 let mut phi = FdMatrix::zeros(n, m);
204 let sqrt2 = 2.0_f64.sqrt();
205
206 for (i, &ti) in t.iter().enumerate() {
207 for j in 0..m {
208 let k = (j + 1) as f64;
209 phi[(i, j)] = sqrt2 * ((k - 0.5) * PI * ti).sin();
211 }
212 }
213 phi
214}
215
216pub fn eigenfunctions(t: &[f64], m: usize, efun_type: EFunType) -> FdMatrix {
226 match efun_type {
227 EFunType::Fourier => fourier_eigenfunctions(t, m),
228 EFunType::Poly => legendre_eigenfunctions(t, m, false),
229 EFunType::PolyHigh => legendre_eigenfunctions(t, m, true),
230 EFunType::Wiener => wiener_eigenfunctions(t, m),
231 }
232}
233
234pub fn eigenvalues_linear(m: usize) -> Vec<f64> {
242 (1..=m).map(|k| 1.0 / k as f64).collect()
243}
244
245pub fn eigenvalues_exponential(m: usize) -> Vec<f64> {
249 (1..=m).map(|k| (-(k as f64)).exp()).collect()
250}
251
252pub fn eigenvalues_wiener(m: usize) -> Vec<f64> {
258 (1..=m)
259 .map(|k| {
260 let denom = (k as f64 - 0.5) * PI;
261 1.0 / (denom * denom)
262 })
263 .collect()
264}
265
266pub fn eigenvalues(m: usize, eval_type: EValType) -> Vec<f64> {
275 match eval_type {
276 EValType::Linear => eigenvalues_linear(m),
277 EValType::Exponential => eigenvalues_exponential(m),
278 EValType::Wiener => eigenvalues_wiener(m),
279 }
280}
281
282pub fn sim_kl(
302 n: usize,
303 phi: &FdMatrix,
304 big_m: usize,
305 lambda: &[f64],
306 seed: Option<u64>,
307) -> FdMatrix {
308 let m = phi.nrows();
309
310 let mut rng = match seed {
312 Some(s) => StdRng::seed_from_u64(s),
313 None => StdRng::from_entropy(),
314 };
315
316 let normal = Normal::new(0.0, 1.0).expect("valid distribution parameters");
317
318 let mut xi = vec![0.0; n * big_m];
321 for k in 0..big_m {
322 let sd = lambda[k].sqrt();
323 for i in 0..n {
324 xi[i + k * n] = rng.sample::<f64, _>(normal) * sd;
325 }
326 }
327
328 let mut data = vec![0.0; n * m];
331
332 maybe_par_chunks_mut_enumerate!(data, n, |(j, col)| {
334 for i in 0..n {
335 let mut sum = 0.0;
336 for k in 0..big_m {
337 sum += xi[i + k * n] * phi[(j, k)];
340 }
341 col[i] = sum;
342 }
343 });
344
345 FdMatrix::from_column_major(data, n, m).expect("dimension invariant: data.len() == n * m")
346}
347
348pub fn sim_fundata(
375 n: usize,
376 t: &[f64],
377 big_m: usize,
378 efun_type: EFunType,
379 eval_type: EValType,
380 seed: Option<u64>,
381) -> FdMatrix {
382 let phi = eigenfunctions(t, big_m, efun_type);
383 let lambda = eigenvalues(big_m, eval_type);
384 sim_kl(n, &phi, big_m, &lambda, seed)
385}
386
387pub fn add_error_pointwise(data: &FdMatrix, sd: f64, seed: Option<u64>) -> FdMatrix {
403 let mut rng = match seed {
404 Some(s) => StdRng::seed_from_u64(s),
405 None => StdRng::from_entropy(),
406 };
407
408 let normal = Normal::new(0.0, sd).expect("valid distribution parameters: sd > 0");
409
410 let noisy: Vec<f64> = data
411 .as_slice()
412 .iter()
413 .map(|&x| x + rng.sample::<f64, _>(normal))
414 .collect();
415
416 FdMatrix::from_column_major(noisy, data.nrows(), data.ncols())
417 .expect("dimension invariant: data.len() == n * m")
418}
419
420pub fn add_error_curve(data: &FdMatrix, sd: f64, seed: Option<u64>) -> FdMatrix {
433 let n = data.nrows();
434 let m = data.ncols();
435
436 let mut rng = match seed {
437 Some(s) => StdRng::seed_from_u64(s),
438 None => StdRng::from_entropy(),
439 };
440
441 let normal = Normal::new(0.0, sd).expect("valid distribution parameters: sd > 0");
442
443 let curve_noise: Vec<f64> = (0..n).map(|_| rng.sample::<f64, _>(normal)).collect();
445
446 let mut result = data.as_slice().to_vec();
448 for j in 0..m {
449 for i in 0..n {
450 result[i + j * n] += curve_noise[i];
451 }
452 }
453 FdMatrix::from_column_major(result, n, m).expect("dimension invariant: data.len() == n * m")
454}
455
456#[derive(Debug, Clone, PartialEq)]
463#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
464#[non_exhaustive]
465pub struct FvarmaResult {
466 pub curves: FdMatrix,
468 pub ar_order: usize,
470 pub ma_order: usize,
472 pub burn_in: usize,
474}
475
476#[derive(Debug, Clone, PartialEq)]
481#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
482#[non_exhaustive]
483pub struct FarmaResult {
484 pub curves: FdMatrix,
486 pub ar_order: usize,
488 pub ma_order: usize,
490 pub burn_in: usize,
492}
493
494fn validate_operator_kernels(
496 argvals: &[f64],
497 ar_ops: &[Vec<f64>],
498 ma_ops: &[Vec<f64>],
499) -> Result<usize, crate::FdarError> {
500 let m = argvals.len();
501 if m == 0 {
502 return Err(crate::FdarError::InvalidDimension {
503 parameter: "argvals",
504 expected: "non-empty grid".to_string(),
505 actual: "0 elements".to_string(),
506 });
507 }
508 for a in ar_ops {
509 if a.len() != m * m {
510 return Err(crate::FdarError::InvalidDimension {
511 parameter: "ar_ops",
512 expected: format!("{} elements per kernel (m*m)", m * m),
513 actual: format!("{} elements", a.len()),
514 });
515 }
516 }
517 for b in ma_ops {
518 if b.len() != m * m {
519 return Err(crate::FdarError::InvalidDimension {
520 parameter: "ma_ops",
521 expected: format!("{} elements per kernel (m*m)", m * m),
522 actual: format!("{} elements", b.len()),
523 });
524 }
525 }
526 Ok(m)
527}
528
529fn fvarma_core(
536 n: usize,
537 argvals: &[f64],
538 ar_ops: &[Vec<f64>],
539 ma_ops: &[Vec<f64>],
540 burn_in: usize,
541 seed: u64,
542) -> Result<FdMatrix, crate::FdarError> {
543 let m = validate_operator_kernels(argvals, ar_ops, ma_ops)?;
544 let p = ar_ops.len();
545 let q = ma_ops.len();
546
547 let mut rng = StdRng::seed_from_u64(seed);
548 let normal = Normal::new(0.0, 1.0).expect("valid distribution parameters");
549
550 let mut hist_x: Vec<Vec<f64>> = Vec::with_capacity(p);
552 let mut hist_eps: Vec<Vec<f64>> = Vec::with_capacity(q);
553 let mut kept: Vec<Vec<f64>> = Vec::with_capacity(n);
554
555 let total = burn_in + n;
556 for step in 0..total {
557 let eps_t: Vec<f64> = (0..m).map(|_| rng.sample::<f64, _>(normal)).collect();
558 let mut x_new = eps_t.clone();
559
560 for (k, a_k) in ar_ops.iter().enumerate() {
562 if let Some(x_prev) = hist_x.get(k) {
563 for j1 in 0..m {
564 let mut s = 0.0;
565 for j2 in 0..m {
566 s += a_k[j1 + j2 * m] * x_prev[j2];
567 }
568 x_new[j1] += s;
569 }
570 }
571 }
572 for (k, b_k) in ma_ops.iter().enumerate() {
574 if let Some(e_prev) = hist_eps.get(k) {
575 for j1 in 0..m {
576 let mut s = 0.0;
577 for j2 in 0..m {
578 s += b_k[j1 + j2 * m] * e_prev[j2];
579 }
580 x_new[j1] += s;
581 }
582 }
583 }
584
585 if x_new.iter().any(|v| !v.is_finite()) {
589 let phase = if step < burn_in { "burn-in" } else { "output" };
590 return Err(crate::FdarError::ComputationFailed {
591 operation: "sim_fvarma recurrence",
592 detail: format!(
593 "curve values diverged to NaN/Inf during {phase} (step {step}); \
594 ensure AR operators have spectral radius < 1"
595 ),
596 });
597 }
598
599 if p > 0 {
600 hist_x.insert(0, x_new.clone());
601 if hist_x.len() > p {
602 hist_x.pop();
603 }
604 }
605 if q > 0 {
606 hist_eps.insert(0, eps_t);
607 if hist_eps.len() > q {
608 hist_eps.pop();
609 }
610 }
611
612 if step >= burn_in {
613 kept.push(x_new);
614 }
615 }
616
617 let mut data = vec![0.0f64; n * m];
619 for (i, curve) in kept.iter().enumerate() {
620 for (j, &v) in curve.iter().enumerate() {
621 data[i + j * n] = v;
622 }
623 }
624 FdMatrix::from_column_major(data, n, m).map_err(|_| crate::FdarError::ComputationFailed {
625 operation: "sim_fvarma assembly",
626 detail: format!("could not assemble {n}×{m} curve matrix"),
627 })
628}
629
630#[must_use = "the simulated curve series is the return value and should be used"]
667pub fn sim_fvarma(
668 n: usize,
669 argvals: &[f64],
670 ar_ops: &[Vec<f64>],
671 ma_ops: &[Vec<f64>],
672 burn_in: usize,
673 seed: u64,
674) -> Result<FvarmaResult, crate::FdarError> {
675 let curves = fvarma_core(n, argvals, ar_ops, ma_ops, burn_in, seed)?;
676 Ok(FvarmaResult {
677 curves,
678 ar_order: ar_ops.len(),
679 ma_order: ma_ops.len(),
680 burn_in,
681 })
682}
683
684#[must_use = "the simulated curve series is the return value and should be used"]
695pub fn sim_farma(
696 n: usize,
697 argvals: &[f64],
698 ar_ops: &[Vec<f64>],
699 ma_ops: &[Vec<f64>],
700 burn_in: usize,
701 seed: u64,
702) -> Result<FarmaResult, crate::FdarError> {
703 let curves = fvarma_core(n, argvals, ar_ops, ma_ops, burn_in, seed)?;
704 Ok(FarmaResult {
705 curves,
706 ar_order: ar_ops.len(),
707 ma_order: ma_ops.len(),
708 burn_in,
709 })
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn uniform_grid(m: usize) -> Vec<f64> {
719 (0..m).map(|j| j as f64 / (m - 1) as f64).collect()
720 }
721
722 fn lag_autocov_fro(data: &FdMatrix, h: usize) -> f64 {
724 let (n, m) = data.shape();
725 let mut xbar = vec![0.0f64; m];
726 for (j, xb) in xbar.iter_mut().enumerate() {
727 let mut s = 0.0;
728 for i in 0..n {
729 s += data[(i, j)];
730 }
731 *xb = s / n as f64;
732 }
733 let mut fro = 0.0;
734 for j1 in 0..m {
735 for j2 in 0..m {
736 let mut c = 0.0;
737 for i in 0..(n - h) {
738 c += (data[(i, j1)] - xbar[j1]) * (data[(i + h, j2)] - xbar[j2]);
739 }
740 c /= n as f64;
741 fro += c * c;
742 }
743 }
744 fro.sqrt()
745 }
746
747 fn scaled_identity(m: usize, s: f64) -> Vec<f64> {
748 let mut a = vec![0.0f64; m * m];
749 for j in 0..m {
750 a[j + j * m] = s;
751 }
752 a
753 }
754
755 #[test]
756 fn fvarma_deterministic() {
757 let (n, m) = (30, 8);
758 let argvals = uniform_grid(m);
759 let ar = vec![scaled_identity(m, 0.3)];
760 let a = sim_fvarma(n, &argvals, &ar, &[], 50, 42).unwrap();
761 let b = sim_fvarma(n, &argvals, &ar, &[], 50, 42).unwrap();
762 assert_eq!(a, b, "same seed must give bit-identical output");
763 assert_eq!(a.curves.shape(), (n, m));
764 assert!(a.curves.as_slice().iter().all(|x| x.is_finite()));
765 assert_eq!(a.ar_order, 1);
766 assert_eq!(a.ma_order, 0);
767 }
768
769 #[test]
770 fn fvarma_zero_op_white_noise() {
771 let (n, m) = (500, 6);
773 let argvals = uniform_grid(m);
774 let ar = vec![vec![0.0f64; m * m]];
775 let res = sim_fvarma(n, &argvals, &ar, &[], 0, 7).unwrap();
776 assert!(res.curves.as_slice().iter().all(|x| x.is_finite()));
777 let c0 = lag_autocov_fro(&res.curves, 0);
778 let c1 = lag_autocov_fro(&res.curves, 1);
779 assert!(c1 < 0.15 * c0, "lag-1 ACF too large: c1={c1}, c0={c0}");
780 }
781
782 #[test]
783 fn fvarma_rank1_dependence() {
784 let (n, m) = (400, 10);
786 let argvals = uniform_grid(m);
787 let raw: Vec<f64> = argvals
788 .iter()
789 .map(|&t| (std::f64::consts::PI * t).sin())
790 .collect();
791 let norm = raw.iter().map(|x| x * x).sum::<f64>().sqrt();
792 let phi: Vec<f64> = raw.iter().map(|x| x / norm).collect();
793 let mut ar1 = vec![0.0f64; m * m];
794 for j1 in 0..m {
795 for j2 in 0..m {
796 ar1[j1 + j2 * m] = 0.8 * phi[j1] * phi[j2];
797 }
798 }
799 let res = sim_fvarma(n, &argvals, &[ar1], &[], 200, 3).unwrap();
800 let c0 = lag_autocov_fro(&res.curves, 0);
801 let c1 = lag_autocov_fro(&res.curves, 1);
802 assert!(c1 > 0.1 * c0, "lag-1 dependence too weak: c1={c1}, c0={c0}");
803 }
804
805 #[test]
806 fn fvarma_dimension_errors() {
807 let (n, m) = (20, 5);
808 let argvals = uniform_grid(m);
809 assert!(matches!(
811 sim_fvarma(n, &argvals, &[vec![0.0; m * m - 1]], &[], 10, 1),
812 Err(crate::FdarError::InvalidDimension {
813 parameter: "ar_ops",
814 ..
815 })
816 ));
817 assert!(matches!(
819 sim_fvarma(n, &argvals, &[], &[vec![0.0; m * m + 2]], 10, 1),
820 Err(crate::FdarError::InvalidDimension {
821 parameter: "ma_ops",
822 ..
823 })
824 ));
825 assert!(matches!(
827 sim_fvarma(n, &[], &[], &[], 10, 1),
828 Err(crate::FdarError::InvalidDimension {
829 parameter: "argvals",
830 ..
831 })
832 ));
833 }
834
835 #[test]
836 fn fvarma_divergence_guard() {
837 let (n, m) = (10, 4);
839 let argvals = uniform_grid(m);
840 let ar = vec![scaled_identity(m, 2.0)];
841 assert!(matches!(
842 sim_fvarma(n, &argvals, &ar, &[], 2000, 1),
843 Err(crate::FdarError::ComputationFailed {
844 operation: "sim_fvarma recurrence",
845 ..
846 })
847 ));
848 }
849
850 #[test]
851 fn farma_shape_and_order() {
852 let (n, m) = (40, 5);
853 let argvals = uniform_grid(m);
854 let ar = vec![scaled_identity(m, 0.3)];
855 let ma = vec![scaled_identity(m, 0.2)];
856 let res = sim_farma(n, &argvals, &ar, &ma, 50, 9).unwrap();
857 assert_eq!(res.curves.shape(), (n, m));
858 assert!(res.curves.as_slice().iter().all(|x| x.is_finite()));
859 assert_eq!(res.ar_order, 1);
860 assert_eq!(res.ma_order, 1);
861 }
862
863 #[test]
864 fn farma_deterministic() {
865 let (n, m) = (25, 6);
866 let argvals = uniform_grid(m);
867 let ar = vec![scaled_identity(m, 0.4)];
868 let ma = vec![scaled_identity(m, 0.25)];
869 let a = sim_farma(n, &argvals, &ar, &ma, 40, 11).unwrap();
870 let b = sim_farma(n, &argvals, &ar, &ma, 40, 11).unwrap();
871 assert_eq!(a, b);
872 }
873
874 #[test]
875 fn farma_equals_fvarma() {
876 let (n, m) = (30, 6);
878 let argvals = uniform_grid(m);
879 let ar = vec![scaled_identity(m, 0.35)];
880 let ma = vec![scaled_identity(m, 0.2)];
881 let f = sim_fvarma(n, &argvals, &ar, &ma, 60, 77).unwrap();
882 let g = sim_farma(n, &argvals, &ar, &ma, 60, 77).unwrap();
883 assert_eq!(f.curves, g.curves);
884 }
885
886 #[test]
887 fn test_fourier_eigenfunctions_dimensions() {
888 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
889 let phi = fourier_eigenfunctions(&t, 5);
890 assert_eq!(phi.nrows(), 100);
891 assert_eq!(phi.ncols(), 5);
892 assert_eq!(phi.len(), 100 * 5);
893 }
894
895 #[test]
896 fn test_fourier_eigenfunctions_first_is_constant() {
897 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
898 let phi = fourier_eigenfunctions(&t, 3);
899
900 for i in 0..100 {
902 assert!((phi[(i, 0)] - 1.0).abs() < 1e-10);
903 }
904 }
905
906 #[test]
907 fn test_eigenvalues_linear() {
908 let lambda = eigenvalues_linear(5);
909 assert_eq!(lambda.len(), 5);
910 assert!((lambda[0] - 1.0).abs() < 1e-10);
911 assert!((lambda[1] - 0.5).abs() < 1e-10);
912 assert!((lambda[2] - 1.0 / 3.0).abs() < 1e-10);
913 }
914
915 #[test]
916 fn test_eigenvalues_exponential() {
917 let lambda = eigenvalues_exponential(3);
918 assert_eq!(lambda.len(), 3);
919 assert!((lambda[0] - (-1.0_f64).exp()).abs() < 1e-10);
920 assert!((lambda[1] - (-2.0_f64).exp()).abs() < 1e-10);
921 }
922
923 #[test]
924 fn test_sim_kl_dimensions() {
925 let t: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
926 let phi = fourier_eigenfunctions(&t, 5);
927 let lambda = eigenvalues_linear(5);
928
929 let data = sim_kl(10, &phi, 5, &lambda, Some(42));
930 assert_eq!(data.nrows(), 10);
931 assert_eq!(data.ncols(), 50);
932 assert_eq!(data.len(), 10 * 50);
933 }
934
935 #[test]
936 fn test_sim_fundata_dimensions() {
937 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
938 let data = sim_fundata(20, &t, 5, EFunType::Fourier, EValType::Linear, Some(42));
939 assert_eq!(data.nrows(), 20);
940 assert_eq!(data.ncols(), 100);
941 assert_eq!(data.len(), 20 * 100);
942 }
943
944 #[test]
945 fn test_add_error_pointwise() {
946 let raw = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let data = FdMatrix::from_column_major(raw.clone(), 2, 3).unwrap();
948 let noisy = add_error_pointwise(&data, 0.1, Some(42));
949 assert_eq!(noisy.len(), 6);
950 let noisy_slice = noisy.as_slice();
952 for i in 0..6 {
953 assert!((noisy_slice[i] - raw[i]).abs() < 1.0);
954 }
955 }
956
957 #[test]
958 fn test_legendre_orthonormality() {
959 let n = 1000;
961 let t: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
962 let m = 5;
963 let phi = legendre_eigenfunctions(&t, m, false);
964 let dt = 1.0 / (n - 1) as f64;
965
966 for j1 in 0..m {
968 for j2 in 0..m {
969 let mut integral = 0.0;
970 for i in 0..n {
971 integral += phi[(i, j1)] * phi[(i, j2)] * dt;
972 }
973 let expected = if j1 == j2 { 1.0 } else { 0.0 };
974 assert!(
975 (integral - expected).abs() < 0.05,
976 "Orthonormality check failed for ({}, {}): {} vs {}",
977 j1,
978 j2,
979 integral,
980 expected
981 );
982 }
983 }
984 }
985
986 #[test]
991 fn test_wiener_eigenfunctions_dimensions() {
992 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
993 let phi = wiener_eigenfunctions(&t, 7);
994 assert_eq!(phi.nrows(), 100);
995 assert_eq!(phi.ncols(), 7);
996 assert_eq!(phi.len(), 100 * 7);
997 }
998
999 #[test]
1000 fn test_wiener_eigenfunctions_orthonormality() {
1001 let n = 1000;
1004 let t: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
1005 let m = 5;
1006 let phi = wiener_eigenfunctions(&t, m);
1007 let dt = 1.0 / (n - 1) as f64;
1008
1009 for j1 in 0..m {
1010 for j2 in 0..m {
1011 let mut integral = 0.0;
1012 for i in 0..n {
1013 integral += phi[(i, j1)] * phi[(i, j2)] * dt;
1014 }
1015 let expected = if j1 == j2 { 1.0 } else { 0.0 };
1016 assert!(
1017 (integral - expected).abs() < 0.05,
1018 "Wiener orthonormality failed for ({}, {}): {} vs {}",
1019 j1,
1020 j2,
1021 integral,
1022 expected
1023 );
1024 }
1025 }
1026 }
1027
1028 #[test]
1029 fn test_wiener_eigenfunctions_analytical_form() {
1030 let t = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1032 let phi = wiener_eigenfunctions(&t, 2);
1033 let sqrt2 = 2.0_f64.sqrt();
1034
1035 for (i, &ti) in t.iter().enumerate() {
1037 let expected = sqrt2 * (0.5 * PI * ti).sin();
1038 assert!(
1039 (phi[(i, 0)] - expected).abs() < 1e-10,
1040 "k=1 at t={}: got {} expected {}",
1041 ti,
1042 phi[(i, 0)],
1043 expected
1044 );
1045 }
1046
1047 for (i, &ti) in t.iter().enumerate() {
1049 let expected = sqrt2 * (1.5 * PI * ti).sin();
1050 assert!(
1051 (phi[(i, 1)] - expected).abs() < 1e-10,
1052 "k=2 at t={}: got {} expected {}",
1053 ti,
1054 phi[(i, 1)],
1055 expected
1056 );
1057 }
1058 }
1059
1060 #[test]
1065 fn test_eigenvalues_wiener_decay_rate() {
1066 let lambda = eigenvalues_wiener(5);
1068 assert_eq!(lambda.len(), 5);
1069
1070 for k in 1..=5 {
1071 let denom = (k as f64 - 0.5) * PI;
1072 let expected = 1.0 / (denom * denom);
1073 assert!(
1074 (lambda[k - 1] - expected).abs() < 1e-12,
1075 "Wiener eigenvalue k={}: got {} expected {}",
1076 k,
1077 lambda[k - 1],
1078 expected
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn test_eigenvalues_wiener_decreasing() {
1085 let lambda = eigenvalues_wiener(10);
1087
1088 for i in 1..lambda.len() {
1089 assert!(
1090 lambda[i] < lambda[i - 1],
1091 "Eigenvalues not decreasing at {}: {} >= {}",
1092 i,
1093 lambda[i],
1094 lambda[i - 1]
1095 );
1096 }
1097 }
1098
1099 #[test]
1104 fn test_add_error_curve_properties() {
1105 let raw = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let n = 2;
1108 let data = FdMatrix::from_column_major(raw.clone(), n, 3).unwrap();
1109 let noisy = add_error_curve(&data, 0.5, Some(42));
1110
1111 assert_eq!(noisy.len(), 6);
1112
1113 let diff0_j0 = noisy[(0, 0)] - raw[0]; let diff0_j1 = noisy[(0, 1)] - raw[n]; let diff0_j2 = noisy[(0, 2)] - raw[2 * n]; assert!(
1120 (diff0_j0 - diff0_j1).abs() < 1e-10,
1121 "Curve 0 noise differs: {} vs {}",
1122 diff0_j0,
1123 diff0_j1
1124 );
1125 assert!(
1126 (diff0_j0 - diff0_j2).abs() < 1e-10,
1127 "Curve 0 noise differs: {} vs {}",
1128 diff0_j0,
1129 diff0_j2
1130 );
1131
1132 let diff1_j0 = noisy[(1, 0)] - raw[1];
1134 assert!(
1137 (diff0_j0 - diff1_j0).abs() > 1e-10,
1138 "Different curves got same noise"
1139 );
1140 }
1141
1142 #[test]
1143 fn test_add_error_curve_reproducibility() {
1144 let raw = vec![1.0, 2.0, 3.0, 4.0];
1145 let data = FdMatrix::from_column_major(raw, 2, 2).unwrap();
1146 let noisy1 = add_error_curve(&data, 1.0, Some(123));
1147 let noisy2 = add_error_curve(&data, 1.0, Some(123));
1148
1149 let s1 = noisy1.as_slice();
1150 let s2 = noisy2.as_slice();
1151 for i in 0..4 {
1152 assert!(
1153 (s1[i] - s2[i]).abs() < 1e-10,
1154 "Reproducibility failed at {}: {} vs {}",
1155 i,
1156 s1[i],
1157 s2[i]
1158 );
1159 }
1160 }
1161
1162 #[test]
1167 fn test_efun_type_from_i32() {
1168 assert_eq!(EFunType::from_i32(0), Ok(EFunType::Fourier));
1169 assert_eq!(EFunType::from_i32(1), Ok(EFunType::Poly));
1170 assert_eq!(EFunType::from_i32(2), Ok(EFunType::PolyHigh));
1171 assert_eq!(EFunType::from_i32(3), Ok(EFunType::Wiener));
1172 assert!(EFunType::from_i32(-1).is_err());
1173 assert!(EFunType::from_i32(4).is_err());
1174 assert!(EFunType::from_i32(100).is_err());
1175 }
1176
1177 #[test]
1178 fn test_eval_type_from_i32() {
1179 assert_eq!(EValType::from_i32(0), Ok(EValType::Linear));
1180 assert_eq!(EValType::from_i32(1), Ok(EValType::Exponential));
1181 assert_eq!(EValType::from_i32(2), Ok(EValType::Wiener));
1182 assert!(EValType::from_i32(-1).is_err());
1183 assert!(EValType::from_i32(3).is_err());
1184 assert!(EValType::from_i32(99).is_err());
1185 }
1186
1187 #[test]
1188 fn test_eigenfunctions_dispatcher() {
1189 let t: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
1190 let m = 4;
1191
1192 let phi_fourier = eigenfunctions(&t, m, EFunType::Fourier);
1194 let phi_fourier_direct = fourier_eigenfunctions(&t, m);
1195 assert_eq!(phi_fourier, phi_fourier_direct);
1196
1197 let phi_poly = eigenfunctions(&t, m, EFunType::Poly);
1198 let phi_poly_direct = legendre_eigenfunctions(&t, m, false);
1199 assert_eq!(phi_poly, phi_poly_direct);
1200
1201 let phi_poly_high = eigenfunctions(&t, m, EFunType::PolyHigh);
1202 let phi_poly_high_direct = legendre_eigenfunctions(&t, m, true);
1203 assert_eq!(phi_poly_high, phi_poly_high_direct);
1204
1205 let phi_wiener = eigenfunctions(&t, m, EFunType::Wiener);
1206 let phi_wiener_direct = wiener_eigenfunctions(&t, m);
1207 assert_eq!(phi_wiener, phi_wiener_direct);
1208 }
1209
1210 #[test]
1211 fn test_sigma_zero_error() {
1212 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1213 let data = sim_fundata(5, &t, 3, EFunType::Fourier, EValType::Exponential, Some(42));
1214 let noisy = add_error_pointwise(&data, 0.0, Some(42));
1215 for i in 0..5 {
1217 for j in 0..20 {
1218 assert!(
1219 (noisy[(i, j)] - data[(i, j)]).abs() < 1e-12,
1220 "Zero-sigma error should not change data"
1221 );
1222 }
1223 }
1224 }
1225
1226 #[test]
1227 fn test_ncomp1_eigenfunctions() {
1228 let t: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
1229 let phi = fourier_eigenfunctions(&t, 1);
1230 assert_eq!(phi.nrows(), t.len());
1231 assert_eq!(phi.ncols(), 1);
1232 let first_val = phi[(0, 0)];
1234 for i in 1..t.len() {
1235 assert!((phi[(i, 0)] - first_val).abs() < 1e-10);
1236 }
1237 }
1238
1239 #[test]
1240 fn test_deterministic_seed() {
1241 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
1242 let d1 = sim_fundata(10, &t, 3, EFunType::Fourier, EValType::Linear, Some(123));
1243 let d2 = sim_fundata(10, &t, 3, EFunType::Fourier, EValType::Linear, Some(123));
1244 for i in 0..10 {
1245 for j in 0..30 {
1246 assert!(
1247 (d1[(i, j)] - d2[(i, j)]).abs() < 1e-12,
1248 "Same seed should produce identical results"
1249 );
1250 }
1251 }
1252 }
1253}