fdars_core/simulation.rs
1//! Simulation functions for functional data.
2//!
3//! This module provides tools for generating synthetic functional data using
4//! the Karhunen-Loève expansion and various eigenfunction/eigenvalue configurations.
5//!
6//! ## Overview
7//!
8//! Functional data can be simulated using the truncated Karhunen-Loève representation:
9//! ```text
10//! f_i(t) = μ(t) + Σ_{k=1}^{M} ξ_{ik} φ_k(t)
11//! ```
12//! where:
13//! - μ(t) is the mean function
14//! - φ_k(t) are orthonormal eigenfunctions
15//! - ξ_{ik} ~ N(0, λ_k) are random scores with variances given by eigenvalues
16//!
17//! ## Eigenfunction Types
18//!
19//! - **Fourier**: sin/cos basis functions, suitable for periodic data
20//! - **Legendre**: Orthonormal Legendre polynomials on \[0,1\]
21//! - **Wiener**: Eigenfunctions of the Wiener process
22
23use rand::prelude::*;
24use rand_distr::Normal;
25use rayon::prelude::*;
26use std::f64::consts::PI;
27
28/// Eigenfunction type enum for simulation
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum EFunType {
31 /// Fourier basis: 1, sqrt(2)*cos(2πkt), sqrt(2)*sin(2πkt)
32 Fourier = 0,
33 /// Orthonormal Legendre polynomials on \[0,1\]
34 Poly = 1,
35 /// Higher-order Legendre polynomials (starting at degree 2)
36 PolyHigh = 2,
37 /// Wiener process eigenfunctions: sqrt(2)*sin((k-0.5)πt)
38 Wiener = 3,
39}
40
41impl EFunType {
42 /// Create from integer (for FFI)
43 pub fn from_i32(value: i32) -> Option<Self> {
44 match value {
45 0 => Some(EFunType::Fourier),
46 1 => Some(EFunType::Poly),
47 2 => Some(EFunType::PolyHigh),
48 3 => Some(EFunType::Wiener),
49 _ => None,
50 }
51 }
52}
53
54/// Eigenvalue decay type for simulation
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub enum EValType {
57 /// Linear decay: λ_k = 1/k
58 Linear = 0,
59 /// Exponential decay: λ_k = exp(-k)
60 Exponential = 1,
61 /// Wiener eigenvalues: λ_k = 1/((k-0.5)π)²
62 Wiener = 2,
63}
64
65impl EValType {
66 /// Create from integer (for FFI)
67 pub fn from_i32(value: i32) -> Option<Self> {
68 match value {
69 0 => Some(EValType::Linear),
70 1 => Some(EValType::Exponential),
71 2 => Some(EValType::Wiener),
72 _ => None,
73 }
74 }
75}
76
77// =============================================================================
78// Eigenfunction Computation
79// =============================================================================
80
81/// Compute Fourier eigenfunctions on \[0,1\].
82///
83/// The Fourier basis consists of:
84/// - φ_1(t) = 1
85/// - φ_{2k}(t) = √2 cos(2πkt) for k = 1, 2, ...
86/// - φ_{2k+1}(t) = √2 sin(2πkt) for k = 1, 2, ...
87///
88/// # Arguments
89/// * `t` - Evaluation points in \[0,1\]
90/// * `m` - Number of eigenfunctions
91///
92/// # Returns
93/// Column-major matrix of size `len(t) × m` as flat vector
94pub fn fourier_eigenfunctions(t: &[f64], m: usize) -> Vec<f64> {
95 let n = t.len();
96 let mut phi = vec![0.0; n * m];
97 let sqrt2 = 2.0_f64.sqrt();
98
99 for (i, &ti) in t.iter().enumerate() {
100 // φ_1(t) = 1
101 phi[i] = 1.0;
102
103 let mut k = 1; // current eigenfunction index
104 let mut freq = 1; // frequency index
105
106 while k < m {
107 // sin term: sqrt(2) * sin(2*pi*freq*t)
108 if k < m {
109 phi[i + k * n] = sqrt2 * (2.0 * PI * freq as f64 * ti).sin();
110 k += 1;
111 }
112 // cos term: sqrt(2) * cos(2*pi*freq*t)
113 if k < m {
114 phi[i + k * n] = sqrt2 * (2.0 * PI * freq as f64 * ti).cos();
115 k += 1;
116 }
117 freq += 1;
118 }
119 }
120 phi
121}
122
123/// Compute Legendre polynomial eigenfunctions on \[0,1\].
124///
125/// Uses orthonormalized Legendre polynomials. The normalization factor is
126/// √(2n+1) where n is the polynomial degree, which ensures unit L² norm on \[0,1\].
127///
128/// # Arguments
129/// * `t` - Evaluation points in \[0,1\]
130/// * `m` - Number of eigenfunctions
131/// * `high` - If true, start at degree 2 (PolyHigh), otherwise start at degree 0
132///
133/// # Returns
134/// Column-major matrix of size `len(t) × m` as flat vector
135pub fn legendre_eigenfunctions(t: &[f64], m: usize, high: bool) -> Vec<f64> {
136 let n = t.len();
137 let mut phi = vec![0.0; n * m];
138 let start_deg = if high { 2 } else { 0 };
139
140 for (i, &ti) in t.iter().enumerate() {
141 // Transform from \[0,1\] to \[-1,1\]
142 let x = 2.0 * ti - 1.0;
143
144 for j in 0..m {
145 let deg = start_deg + j;
146 // Compute Legendre polynomial P_deg(x)
147 let p = legendre_p(x, deg);
148 // Normalize: ||P_n||² on \[-1,1\] = 2/(2n+1), on \[0,1\] = 1/(2n+1)
149 let norm = ((2 * deg + 1) as f64).sqrt();
150 phi[i + j * n] = p * norm;
151 }
152 }
153 phi
154}
155
156/// Compute Legendre polynomial P_n(x) using recurrence relation.
157///
158/// The three-term recurrence is:
159/// (n+1)P_{n+1}(x) = (2n+1)xP_n(x) - nP_{n-1}(x)
160fn legendre_p(x: f64, n: usize) -> f64 {
161 if n == 0 {
162 return 1.0;
163 }
164 if n == 1 {
165 return x;
166 }
167
168 let mut p_prev = 1.0;
169 let mut p_curr = x;
170
171 for k in 2..=n {
172 let p_next = ((2 * k - 1) as f64 * x * p_curr - (k - 1) as f64 * p_prev) / k as f64;
173 p_prev = p_curr;
174 p_curr = p_next;
175 }
176 p_curr
177}
178
179/// Compute Wiener process eigenfunctions on \[0,1\].
180///
181/// The Wiener (Brownian motion) eigenfunctions are:
182/// φ_k(t) = √2 sin((k - 0.5)πt)
183///
184/// These are the eigenfunctions of the covariance kernel K(s,t) = min(s,t).
185///
186/// # Arguments
187/// * `t` - Evaluation points in \[0,1\]
188/// * `m` - Number of eigenfunctions
189///
190/// # Returns
191/// Column-major matrix of size `len(t) × m` as flat vector
192pub fn wiener_eigenfunctions(t: &[f64], m: usize) -> Vec<f64> {
193 let n = t.len();
194 let mut phi = vec![0.0; n * m];
195 let sqrt2 = 2.0_f64.sqrt();
196
197 for (i, &ti) in t.iter().enumerate() {
198 for j in 0..m {
199 let k = (j + 1) as f64;
200 // φ_k(t) = sqrt(2) * sin((k - 0.5) * pi * t)
201 phi[i + j * n] = sqrt2 * ((k - 0.5) * PI * ti).sin();
202 }
203 }
204 phi
205}
206
207/// Unified eigenfunction computation.
208///
209/// # Arguments
210/// * `t` - Evaluation points
211/// * `m` - Number of eigenfunctions
212/// * `efun_type` - Type of eigenfunction basis
213///
214/// # Returns
215/// Column-major matrix of size `len(t) × m` as flat vector
216pub fn eigenfunctions(t: &[f64], m: usize, efun_type: EFunType) -> Vec<f64> {
217 match efun_type {
218 EFunType::Fourier => fourier_eigenfunctions(t, m),
219 EFunType::Poly => legendre_eigenfunctions(t, m, false),
220 EFunType::PolyHigh => legendre_eigenfunctions(t, m, true),
221 EFunType::Wiener => wiener_eigenfunctions(t, m),
222 }
223}
224
225// =============================================================================
226// Eigenvalue Computation
227// =============================================================================
228
229/// Generate eigenvalue sequence with linear decay.
230///
231/// λ_k = 1/k for k = 1, ..., m
232pub fn eigenvalues_linear(m: usize) -> Vec<f64> {
233 (1..=m).map(|k| 1.0 / k as f64).collect()
234}
235
236/// Generate eigenvalue sequence with exponential decay.
237///
238/// λ_k = exp(-k) for k = 1, ..., m
239pub fn eigenvalues_exponential(m: usize) -> Vec<f64> {
240 (1..=m).map(|k| (-(k as f64)).exp()).collect()
241}
242
243/// Generate Wiener process eigenvalues.
244///
245/// λ_k = 1/((k - 0.5)π)² for k = 1, ..., m
246///
247/// These are the eigenvalues of the covariance kernel K(s,t) = min(s,t).
248pub fn eigenvalues_wiener(m: usize) -> Vec<f64> {
249 (1..=m)
250 .map(|k| {
251 let denom = (k as f64 - 0.5) * PI;
252 1.0 / (denom * denom)
253 })
254 .collect()
255}
256
257/// Unified eigenvalue computation.
258///
259/// # Arguments
260/// * `m` - Number of eigenvalues
261/// * `eval_type` - Type of eigenvalue decay
262///
263/// # Returns
264/// Vector of m eigenvalues in decreasing order
265pub fn eigenvalues(m: usize, eval_type: EValType) -> Vec<f64> {
266 match eval_type {
267 EValType::Linear => eigenvalues_linear(m),
268 EValType::Exponential => eigenvalues_exponential(m),
269 EValType::Wiener => eigenvalues_wiener(m),
270 }
271}
272
273// =============================================================================
274// Karhunen-Loève Simulation
275// =============================================================================
276
277/// Simulate functional data via Karhunen-Loève expansion.
278///
279/// Generates n curves using the truncated KL representation:
280/// f_i(t) = Σ_{k=1}^{M} ξ_{ik} φ_k(t)
281/// where ξ_{ik} ~ N(0, λ_k)
282///
283/// # Arguments
284/// * `n` - Number of curves to generate
285/// * `phi` - Eigenfunctions matrix (m × M) in column-major format
286/// * `m` - Number of evaluation points
287/// * `big_m` - Number of eigenfunctions
288/// * `lambda` - Eigenvalues (length M)
289/// * `seed` - Optional random seed for reproducibility
290///
291/// # Returns
292/// Data matrix (n × m) in column-major format
293pub fn sim_kl(
294 n: usize,
295 phi: &[f64],
296 m: usize,
297 big_m: usize,
298 lambda: &[f64],
299 seed: Option<u64>,
300) -> Vec<f64> {
301 // Create RNG
302 let mut rng = match seed {
303 Some(s) => StdRng::seed_from_u64(s),
304 None => StdRng::from_entropy(),
305 };
306
307 let normal = Normal::new(0.0, 1.0).unwrap();
308
309 // Generate scores ξ ~ N(0, λ) for all curves
310 // xi is n × big_m in column-major format
311 let mut xi = vec![0.0; n * big_m];
312 for k in 0..big_m {
313 let sd = lambda[k].sqrt();
314 for i in 0..n {
315 xi[i + k * n] = rng.sample::<f64, _>(normal) * sd;
316 }
317 }
318
319 // Compute data = xi * phi^T
320 // xi: n × M, phi: m × M -> data: n × m
321 let mut data = vec![0.0; n * m];
322
323 // Parallelize over columns (evaluation points)
324 data.par_chunks_mut(n).enumerate().for_each(|(j, col)| {
325 for i in 0..n {
326 let mut sum = 0.0;
327 for k in 0..big_m {
328 // phi[j + k*m] is φ_k(t_j)
329 // xi[i + k*n] is ξ_{ik}
330 sum += xi[i + k * n] * phi[j + k * m];
331 }
332 col[i] = sum;
333 }
334 });
335
336 data
337}
338
339/// Simulate functional data with specified eigenfunction and eigenvalue types.
340///
341/// Convenience function that combines eigenfunction and eigenvalue generation
342/// with KL simulation.
343///
344/// # Arguments
345/// * `n` - Number of curves to generate
346/// * `t` - Evaluation points
347/// * `big_m` - Number of eigenfunctions/eigenvalues to use
348/// * `efun_type` - Type of eigenfunction basis
349/// * `eval_type` - Type of eigenvalue decay
350/// * `seed` - Optional random seed
351///
352/// # Returns
353/// Data matrix (n × len(t)) in column-major format
354pub fn sim_fundata(
355 n: usize,
356 t: &[f64],
357 big_m: usize,
358 efun_type: EFunType,
359 eval_type: EValType,
360 seed: Option<u64>,
361) -> Vec<f64> {
362 let m = t.len();
363 let phi = eigenfunctions(t, big_m, efun_type);
364 let lambda = eigenvalues(big_m, eval_type);
365 sim_kl(n, &phi, m, big_m, &lambda, seed)
366}
367
368// =============================================================================
369// Noise Addition
370// =============================================================================
371
372/// Add pointwise Gaussian noise to functional data.
373///
374/// Adds independent N(0, σ²) noise to each point.
375///
376/// # Arguments
377/// * `data` - Data matrix (n × m) in column-major format
378/// * `nrow` - Number of rows (observations)
379/// * `ncol` - Number of columns (evaluation points)
380/// * `sd` - Standard deviation of noise
381/// * `seed` - Optional random seed
382///
383/// # Returns
384/// Noisy data matrix (n × m) in column-major format
385pub fn add_error_pointwise(
386 data: &[f64],
387 _nrow: usize,
388 _ncol: usize,
389 sd: f64,
390 seed: Option<u64>,
391) -> Vec<f64> {
392 let mut rng = match seed {
393 Some(s) => StdRng::seed_from_u64(s),
394 None => StdRng::from_entropy(),
395 };
396
397 let normal = Normal::new(0.0, sd).unwrap();
398
399 data.iter()
400 .map(|&x| x + rng.sample::<f64, _>(normal))
401 .collect()
402}
403
404/// Add curve-level Gaussian noise to functional data.
405///
406/// Adds a constant noise term per curve: each observation in curve i
407/// has the same noise value.
408///
409/// # Arguments
410/// * `data` - Data matrix (n × m) in column-major format
411/// * `nrow` - Number of rows (observations)
412/// * `ncol` - Number of columns (evaluation points)
413/// * `sd` - Standard deviation of noise
414/// * `seed` - Optional random seed
415///
416/// # Returns
417/// Noisy data matrix (n × m) in column-major format
418pub fn add_error_curve(
419 data: &[f64],
420 nrow: usize,
421 ncol: usize,
422 sd: f64,
423 seed: Option<u64>,
424) -> Vec<f64> {
425 let mut rng = match seed {
426 Some(s) => StdRng::seed_from_u64(s),
427 None => StdRng::from_entropy(),
428 };
429
430 let normal = Normal::new(0.0, sd).unwrap();
431
432 // Generate one noise value per curve
433 let curve_noise: Vec<f64> = (0..nrow).map(|_| rng.sample::<f64, _>(normal)).collect();
434
435 // Add to data
436 let mut result = data.to_vec();
437 for j in 0..ncol {
438 for i in 0..nrow {
439 result[i + j * nrow] += curve_noise[i];
440 }
441 }
442 result
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_fourier_eigenfunctions_dimensions() {
451 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
452 let phi = fourier_eigenfunctions(&t, 5);
453 assert_eq!(phi.len(), 100 * 5);
454 }
455
456 #[test]
457 fn test_fourier_eigenfunctions_first_is_constant() {
458 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
459 let phi = fourier_eigenfunctions(&t, 3);
460
461 // First eigenfunction should be constant 1
462 for i in 0..100 {
463 assert!((phi[i] - 1.0).abs() < 1e-10);
464 }
465 }
466
467 #[test]
468 fn test_eigenvalues_linear() {
469 let lambda = eigenvalues_linear(5);
470 assert_eq!(lambda.len(), 5);
471 assert!((lambda[0] - 1.0).abs() < 1e-10);
472 assert!((lambda[1] - 0.5).abs() < 1e-10);
473 assert!((lambda[2] - 1.0 / 3.0).abs() < 1e-10);
474 }
475
476 #[test]
477 fn test_eigenvalues_exponential() {
478 let lambda = eigenvalues_exponential(3);
479 assert_eq!(lambda.len(), 3);
480 assert!((lambda[0] - (-1.0_f64).exp()).abs() < 1e-10);
481 assert!((lambda[1] - (-2.0_f64).exp()).abs() < 1e-10);
482 }
483
484 #[test]
485 fn test_sim_kl_dimensions() {
486 let t: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
487 let phi = fourier_eigenfunctions(&t, 5);
488 let lambda = eigenvalues_linear(5);
489
490 let data = sim_kl(10, &phi, 50, 5, &lambda, Some(42));
491 assert_eq!(data.len(), 10 * 50);
492 }
493
494 #[test]
495 fn test_sim_fundata_dimensions() {
496 let t: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
497 let data = sim_fundata(20, &t, 5, EFunType::Fourier, EValType::Linear, Some(42));
498 assert_eq!(data.len(), 20 * 100);
499 }
500
501 #[test]
502 fn test_add_error_pointwise() {
503 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 2 x 3 matrix
504 let noisy = add_error_pointwise(&data, 2, 3, 0.1, Some(42));
505 assert_eq!(noisy.len(), 6);
506 // Check that values changed but not by too much
507 for i in 0..6 {
508 assert!((noisy[i] - data[i]).abs() < 1.0);
509 }
510 }
511
512 #[test]
513 fn test_legendre_orthonormality() {
514 // Test that Legendre eigenfunctions are approximately orthonormal
515 let n = 1000;
516 let t: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
517 let m = 5;
518 let phi = legendre_eigenfunctions(&t, m, false);
519 let dt = 1.0 / (n - 1) as f64;
520
521 // Check orthonormality
522 for j1 in 0..m {
523 for j2 in 0..m {
524 let mut integral = 0.0;
525 for i in 0..n {
526 integral += phi[i + j1 * n] * phi[i + j2 * n] * dt;
527 }
528 let expected = if j1 == j2 { 1.0 } else { 0.0 };
529 assert!(
530 (integral - expected).abs() < 0.05,
531 "Orthonormality check failed for ({}, {}): {} vs {}",
532 j1,
533 j2,
534 integral,
535 expected
536 );
537 }
538 }
539 }
540}