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}