Skip to main content

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 crate::matrix::FdMatrix;
24use crate::maybe_par_chunks_mut_enumerate;
25use rand::prelude::*;
26use rand_distr::Normal;
27use std::f64::consts::PI;
28
29/// Eigenfunction type enum for simulation
30#[derive(Clone, Copy, Debug, PartialEq)]
31#[non_exhaustive]
32pub enum EFunType {
33    /// Fourier basis: 1, sqrt(2)*cos(2πkt), sqrt(2)*sin(2πkt)
34    Fourier = 0,
35    /// Orthonormal Legendre polynomials on \[0,1\]
36    Poly = 1,
37    /// Higher-order Legendre polynomials (starting at degree 2)
38    PolyHigh = 2,
39    /// Wiener process eigenfunctions: sqrt(2)*sin((k-0.5)πt)
40    Wiener = 3,
41}
42
43impl EFunType {
44    /// Create from integer (for FFI)
45    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/// Eigenvalue decay type for simulation
60#[derive(Clone, Copy, Debug, PartialEq)]
61#[non_exhaustive]
62pub enum EValType {
63    /// Linear decay: λ_k = 1/k
64    Linear = 0,
65    /// Exponential decay: λ_k = exp(-k)
66    Exponential = 1,
67    /// Wiener eigenvalues: λ_k = 1/((k-0.5)π)²
68    Wiener = 2,
69}
70
71impl EValType {
72    /// Create from integer (for FFI)
73    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
86// =============================================================================
87// Eigenfunction Computation
88// =============================================================================
89
90/// Compute Fourier eigenfunctions on \[0,1\].
91///
92/// The Fourier basis consists of:
93/// - φ_1(t) = 1
94/// - φ_{2k}(t) = √2 cos(2πkt) for k = 1, 2, ...
95/// - φ_{2k+1}(t) = √2 sin(2πkt) for k = 1, 2, ...
96///
97/// # Arguments
98/// * `t` - Evaluation points in \[0,1\]
99/// * `m` - Number of eigenfunctions
100///
101/// # Returns
102/// `FdMatrix` of size `len(t) × m`
103pub 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        // φ_1(t) = 1
110        phi[(i, 0)] = 1.0;
111
112        let mut k = 1; // current eigenfunction index
113        let mut freq = 1; // frequency index
114
115        while k < m {
116            // sin term: sqrt(2) * sin(2*pi*freq*t)
117            if k < m {
118                phi[(i, k)] = sqrt2 * (2.0 * PI * f64::from(freq) * ti).sin();
119                k += 1;
120            }
121            // cos term: sqrt(2) * cos(2*pi*freq*t)
122            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
132/// Compute Legendre polynomial eigenfunctions on \[0,1\].
133///
134/// Uses orthonormalized Legendre polynomials. The normalization factor is
135/// √(2n+1) where n is the polynomial degree, which ensures unit L² norm on \[0,1\].
136///
137/// # Arguments
138/// * `t` - Evaluation points in \[0,1\]
139/// * `m` - Number of eigenfunctions
140/// * `high` - If true, start at degree 2 (PolyHigh), otherwise start at degree 0
141///
142/// # Returns
143/// `FdMatrix` of size `len(t) × m`
144pub 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        // Transform from \[0,1\] to \[-1,1\]
151        let x = 2.0 * ti - 1.0;
152
153        for j in 0..m {
154            let deg = start_deg + j;
155            // Compute Legendre polynomial P_deg(x)
156            let p = legendre_p(x, deg);
157            // Normalize: ||P_n||² on \[-1,1\] = 2/(2n+1), on \[0,1\] = 1/(2n+1)
158            let norm = ((2 * deg + 1) as f64).sqrt();
159            phi[(i, j)] = p * norm;
160        }
161    }
162    phi
163}
164
165/// Compute Legendre polynomial P_n(x) using recurrence relation.
166///
167/// The three-term recurrence is:
168/// (n+1)P_{n+1}(x) = (2n+1)xP_n(x) - nP_{n-1}(x)
169fn 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
188/// Compute Wiener process eigenfunctions on \[0,1\].
189///
190/// The Wiener (Brownian motion) eigenfunctions are:
191/// φ_k(t) = √2 sin((k - 0.5)πt)
192///
193/// These are the eigenfunctions of the covariance kernel K(s,t) = min(s,t).
194///
195/// # Arguments
196/// * `t` - Evaluation points in \[0,1\]
197/// * `m` - Number of eigenfunctions
198///
199/// # Returns
200/// `FdMatrix` of size `len(t) × m`
201pub 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            // φ_k(t) = sqrt(2) * sin((k - 0.5) * pi * t)
210            phi[(i, j)] = sqrt2 * ((k - 0.5) * PI * ti).sin();
211        }
212    }
213    phi
214}
215
216/// Unified eigenfunction computation.
217///
218/// # Arguments
219/// * `t` - Evaluation points
220/// * `m` - Number of eigenfunctions
221/// * `efun_type` - Type of eigenfunction basis
222///
223/// # Returns
224/// `FdMatrix` of size `len(t) × m`
225pub 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
234// =============================================================================
235// Eigenvalue Computation
236// =============================================================================
237
238/// Generate eigenvalue sequence with linear decay.
239///
240/// λ_k = 1/k for k = 1, ..., m
241pub fn eigenvalues_linear(m: usize) -> Vec<f64> {
242    (1..=m).map(|k| 1.0 / k as f64).collect()
243}
244
245/// Generate eigenvalue sequence with exponential decay.
246///
247/// λ_k = exp(-k) for k = 1, ..., m
248pub fn eigenvalues_exponential(m: usize) -> Vec<f64> {
249    (1..=m).map(|k| (-(k as f64)).exp()).collect()
250}
251
252/// Generate Wiener process eigenvalues.
253///
254/// λ_k = 1/((k - 0.5)π)² for k = 1, ..., m
255///
256/// These are the eigenvalues of the covariance kernel K(s,t) = min(s,t).
257pub 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
266/// Unified eigenvalue computation.
267///
268/// # Arguments
269/// * `m` - Number of eigenvalues
270/// * `eval_type` - Type of eigenvalue decay
271///
272/// # Returns
273/// Vector of m eigenvalues in decreasing order
274pub 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
282// =============================================================================
283// Karhunen-Loève Simulation
284// =============================================================================
285
286/// Simulate functional data via Karhunen-Loève expansion.
287///
288/// Generates n curves using the truncated KL representation:
289/// f_i(t) = Σ_{k=1}^{M} ξ_{ik} φ_k(t)
290/// where ξ_{ik} ~ N(0, λ_k)
291///
292/// # Arguments
293/// * `n` - Number of curves to generate
294/// * `phi` - Eigenfunctions matrix (m × big_m) as `FdMatrix`
295/// * `big_m` - Number of eigenfunctions
296/// * `lambda` - Eigenvalues (length big_m)
297/// * `seed` - Optional random seed for reproducibility
298///
299/// # Returns
300/// Data `FdMatrix` of size `n × m`
301pub 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    // Create RNG
311    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    // Generate scores ξ ~ N(0, λ) for all curves
319    // xi is n × big_m in column-major format
320    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    // Compute data = xi * phi^T
329    // xi: n × big_m, phi: m × big_m -> data: n × m
330    let mut data = vec![0.0; n * m];
331
332    // Parallelize over columns (evaluation points)
333    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                // phi[(j, k)] is φ_k(t_j)
338                // xi[i + k*n] is ξ_{ik}
339                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
348/// Simulate functional data with specified eigenfunction and eigenvalue types.
349///
350/// Convenience function that combines eigenfunction and eigenvalue generation
351/// with KL simulation.
352///
353/// # Arguments
354/// * `n` - Number of curves to generate
355/// * `t` - Evaluation points
356/// * `big_m` - Number of eigenfunctions/eigenvalues to use
357/// * `efun_type` - Type of eigenfunction basis
358/// * `eval_type` - Type of eigenvalue decay
359/// * `seed` - Optional random seed
360///
361/// # Returns
362/// Data `FdMatrix` of size `n × len(t)`
363///
364/// # Examples
365///
366/// ```
367/// use fdars_core::simulation::{sim_fundata, EFunType, EValType};
368///
369/// let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
370/// let data = sim_fundata(5, &t, 4, EFunType::Fourier, EValType::Linear, Some(42));
371/// assert_eq!(data.shape(), (5, 20));
372/// assert!(data.as_slice().iter().all(|v| v.is_finite()));
373/// ```
374pub 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
387// =============================================================================
388// Noise Addition
389// =============================================================================
390
391/// Add pointwise Gaussian noise to functional data.
392///
393/// Adds independent N(0, σ²) noise to each point.
394///
395/// # Arguments
396/// * `data` - Data `FdMatrix` (n × m)
397/// * `sd` - Standard deviation of noise
398/// * `seed` - Optional random seed
399///
400/// # Returns
401/// Noisy data `FdMatrix` (n × m)
402pub 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
420/// Add curve-level Gaussian noise to functional data.
421///
422/// Adds a constant noise term per curve: each observation in curve i
423/// has the same noise value.
424///
425/// # Arguments
426/// * `data` - Data `FdMatrix` (n × m)
427/// * `sd` - Standard deviation of noise
428/// * `seed` - Optional random seed
429///
430/// # Returns
431/// Noisy data `FdMatrix` (n × m)
432pub 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    // Generate one noise value per curve
444    let curve_noise: Vec<f64> = (0..n).map(|_| rng.sample::<f64, _>(normal)).collect();
445
446    // Add to data
447    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// ─── Functional VAR/VMA + FARMA simulators (FTS-03-04/05, plan 41-02) ─────────
457
458/// Result of a functional VAR/VMA (`sim_fvarma`) simulation.
459///
460/// Produced by [`sim_fvarma`]. `curves` is the `N × m` simulated series (rows =
461/// curves, columns = grid points).
462#[derive(Debug, Clone, PartialEq)]
463#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
464#[non_exhaustive]
465pub struct FvarmaResult {
466    /// Simulated curve series, shape `N × m`.
467    pub curves: FdMatrix,
468    /// Autoregressive order p (= number of AR operator kernels).
469    pub ar_order: usize,
470    /// Moving-average order q (= number of MA operator kernels).
471    pub ma_order: usize,
472    /// Number of burn-in curves discarded before the kept output.
473    pub burn_in: usize,
474}
475
476/// Result of a functional ARMA (`sim_farma`) simulation.
477///
478/// Produced by [`sim_farma`]. Same fields as [`FvarmaResult`]; the two share the
479/// underlying recurrence and are bit-identical for identical inputs.
480#[derive(Debug, Clone, PartialEq)]
481#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
482#[non_exhaustive]
483pub struct FarmaResult {
484    /// Simulated curve series, shape `N × m`.
485    pub curves: FdMatrix,
486    /// Autoregressive order p (= number of AR operator kernels).
487    pub ar_order: usize,
488    /// Moving-average order q (= number of MA operator kernels).
489    pub ma_order: usize,
490    /// Number of burn-in curves discarded before the kept output.
491    pub burn_in: usize,
492}
493
494/// Validate the grid and that every operator kernel is a flat m×m matrix.
495fn 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
529/// Shared functional VAR/VMA/FARMA recurrence used by both public entry points.
530///
531/// Runs `burn_in + n` steps of `X_t = Σ_k A_k·X_{t-k} + ε_t + Σ_k B_k·ε_{t-k}`
532/// with i.i.d. N(0,1) innovations per grid point, discards the first `burn_in`
533/// curves, and returns the kept `n × m` series. Returns `ComputationFailed` if
534/// any curve entry becomes non-finite (non-stationary operator).
535fn 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    // Ring histories: hist_x[k] = X_{t-1-k}, hist_eps[k] = ε_{t-1-k}.
551    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        // AR terms: X_t += A_k · X_{t-1-k}.
561        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        // MA terms: X_t += B_k · ε_{t-1-k}.
573        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        // Divergence guard (Pitfall 5): reject non-finite values instead of emitting them.
586        // Fires anywhere in the recurrence (burn-in or kept output), so the message
587        // stays phase-agnostic.
588        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    // Assemble N×m column-major FdMatrix: data[i + j*n] = kept[i][j].
618    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/// Simulate a functional VAR/VMA process from user-supplied operator kernels.
631///
632/// Generates `n` curves from the recurrence
633/// `X_t = Σ_{k=1}^{p} A_k·X_{t-k} + ε_t + Σ_{k=1}^{q} B_k·ε_{t-k}`,
634/// where each `A_k` (AR) and `B_k` (MA) is a flat column-major m×m operator kernel
635/// applied by matrix-vector product to the grid-discretized curve, and the
636/// innovations `ε_t` are i.i.d. standard-normal per grid point. The first
637/// `burn_in` curves are discarded so the kept output is approximately stationary.
638///
639/// # Arguments
640///
641/// * `n` — number of output curves.
642/// * `argvals` — grid points; `m = argvals.len()`.
643/// * `ar_ops` — AR operator kernels, each a flat m×m column-major matrix (`p = ar_ops.len()`).
644/// * `ma_ops` — MA operator kernels, each a flat m×m column-major matrix (`q = ma_ops.len()`).
645/// * `burn_in` — number of leading curves to discard (200 is a reasonable default
646///   for moderate operator norms).
647/// * `seed` — RNG seed; output is bit-identical for a fixed seed
648///   (`StdRng::seed_from_u64`), with no entropy fallback.
649///
650/// # Errors
651///
652/// [`crate::FdarError::InvalidDimension`] if `argvals` is empty or any kernel is
653/// not m×m; [`crate::FdarError::ComputationFailed`] if the recurrence diverges to
654/// NaN/Inf (a non-stationary operator).
655///
656/// # Stationarity
657///
658/// Stationarity (companion-matrix spectral radius < 1; sufficient condition
659/// `‖A_1‖_HS < 1` for FAR(1)) is the caller's responsibility — it is not enforced,
660/// only guarded against numeric divergence.
661///
662/// # Divergence from `freqdom`
663///
664/// Innovations use an identity covariance (i.i.d. N(0,1) per grid point);
665/// `freqdom::fts.rar` accepts a user-supplied innovation covariance σ.
666#[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/// Simulate a functional ARMA (FARMA) process combining AR and MA operator terms.
685///
686/// FARMA is the combined AR+MA case of the functional operator recurrence; this is
687/// a thin named entry point over the shared [`sim_fvarma`] recurrence and is
688/// bit-identical to `sim_fvarma` for identical inputs. See [`sim_fvarma`] for the
689/// recurrence, stationarity responsibility, and R-baseline divergence.
690///
691/// # Errors
692///
693/// Same as [`sim_fvarma`].
694#[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    // ── FTS-03-04/05 simulator helpers + oracles ────────────────────────────
717
718    fn uniform_grid(m: usize) -> Vec<f64> {
719        (0..m).map(|j| j as f64 / (m - 1) as f64).collect()
720    }
721
722    /// Frobenius norm of the lag-`h` sample autocovariance operator.
723    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        // Oracle 4: zero AR operator ⇒ pure i.i.d. innovations, near-zero lag-1 ACF.
772        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        // Oracle 6: rank-1 AR operator ⇒ non-trivial lag-1 serial dependence.
785        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        // AR kernel wrong length.
810        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        // MA kernel wrong length.
818        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        // Empty grid.
826        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        // Spectral radius ≥ 1 (2×identity) ⇒ geometric blow-up ⇒ ComputationFailed.
838        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        // FARMA = combined AR+MA: identical inputs + seed ⇒ identical curves.
877        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        // First eigenfunction should be constant 1
901        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]; // 2 x 3 matrix
947        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        // Check that values changed but not by too much
951        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        // Test that Legendre eigenfunctions are approximately orthonormal
960        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        // Check orthonormality
967        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    // ========================================================================
987    // Wiener eigenfunction tests
988    // ========================================================================
989
990    #[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        // Wiener eigenfunctions: sqrt(2)*sin((k-0.5)*pi*t)
1002        // Should be orthonormal on [0,1]
1003        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        // φ_k(t) = sqrt(2) * sin((k - 0.5) * pi * t)
1031        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        // First eigenfunction: k=1, freq = 0.5*pi
1036        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        // Second eigenfunction: k=2, freq = 1.5*pi
1048        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    // ========================================================================
1061    // Wiener eigenvalue tests
1062    // ========================================================================
1063
1064    #[test]
1065    fn test_eigenvalues_wiener_decay_rate() {
1066        // λ_k = 1/((k - 0.5)*pi)^2
1067        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        // Wiener eigenvalues should decrease monotonically
1086        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    // ========================================================================
1100    // add_error_curve tests
1101    // ========================================================================
1102
1103    #[test]
1104    fn test_add_error_curve_properties() {
1105        // Curve-level noise: each observation in same curve gets same noise
1106        let raw = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 2 curves x 3 points
1107        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        // Compute the difference for curve 0 at each point
1114        let diff0_j0 = noisy[(0, 0)] - raw[0]; // curve 0, point 0
1115        let diff0_j1 = noisy[(0, 1)] - raw[n]; // curve 0, point 1
1116        let diff0_j2 = noisy[(0, 2)] - raw[2 * n]; // curve 0, point 2
1117
1118        // All differences for same curve should be equal (same noise added)
1119        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        // Curve 1 should have different noise
1133        let diff1_j0 = noisy[(1, 0)] - raw[1];
1134        // Different curves should (with high probability) have different noise
1135        // We can't guarantee this, but with seed=42 they should differ
1136        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    // ========================================================================
1163    // Enum dispatcher tests
1164    // ========================================================================
1165
1166    #[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        // Test that dispatcher returns correct results for each type
1193        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        // sigma=0 → no noise added, should be identical
1216        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        // First Fourier eigenfunction should be constant
1233        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}