fdars_core/
irreg_fdata.rs

1//! Irregular functional data operations.
2//!
3//! This module provides data structures and algorithms for functional data
4//! where observations have different evaluation points (irregular/sparse sampling).
5//!
6//! ## Storage Format
7//!
8//! Uses a CSR-like (Compressed Sparse Row) format for efficient storage:
9//! - `offsets[i]..offsets[i+1]` gives the slice indices for observation i
10//! - `argvals` and `values` store all data contiguously
11//!
12//! This format is memory-efficient and enables parallel processing of observations.
13//!
14//! ## Example
15//!
16//! For 3 curves with varying numbers of observation points:
17//! - Curve 0: 5 points
18//! - Curve 1: 3 points
19//! - Curve 2: 7 points
20//!
21//! The offsets would be: [0, 5, 8, 15]
22
23use crate::{iter_maybe_parallel, slice_maybe_parallel};
24#[cfg(feature = "parallel")]
25use rayon::iter::ParallelIterator;
26
27/// Compressed storage for irregular functional data.
28///
29/// Uses CSR-style layout where each observation can have a different
30/// number of evaluation points.
31#[derive(Clone, Debug)]
32pub struct IrregFdata {
33    /// Start indices for each observation (length n+1)
34    /// `offsets[i]..offsets[i+1]` gives the range for observation i
35    pub offsets: Vec<usize>,
36    /// All observation points concatenated
37    pub argvals: Vec<f64>,
38    /// All values concatenated
39    pub values: Vec<f64>,
40    /// Domain range `[min, max]`
41    pub rangeval: [f64; 2],
42}
43
44impl IrregFdata {
45    /// Create from lists of argvals and values (one per observation).
46    ///
47    /// # Arguments
48    /// * `argvals_list` - List of observation point vectors
49    /// * `values_list` - List of value vectors (same lengths as argvals_list)
50    ///
51    /// # Panics
52    /// Panics if the lists have different lengths or if any pair has mismatched lengths.
53    pub fn from_lists(argvals_list: &[Vec<f64>], values_list: &[Vec<f64>]) -> Self {
54        let n = argvals_list.len();
55        assert_eq!(
56            n,
57            values_list.len(),
58            "argvals_list and values_list must have same length"
59        );
60
61        let mut offsets = Vec::with_capacity(n + 1);
62        offsets.push(0);
63
64        let total_points: usize = argvals_list.iter().map(|v| v.len()).sum();
65        let mut argvals = Vec::with_capacity(total_points);
66        let mut values = Vec::with_capacity(total_points);
67
68        let mut range_min = f64::INFINITY;
69        let mut range_max = f64::NEG_INFINITY;
70
71        for i in 0..n {
72            assert_eq!(
73                argvals_list[i].len(),
74                values_list[i].len(),
75                "Observation {} has mismatched argvals/values lengths",
76                i
77            );
78
79            argvals.extend_from_slice(&argvals_list[i]);
80            values.extend_from_slice(&values_list[i]);
81            offsets.push(argvals.len());
82
83            if let (Some(&min), Some(&max)) = (argvals_list[i].first(), argvals_list[i].last()) {
84                range_min = range_min.min(min);
85                range_max = range_max.max(max);
86            }
87        }
88
89        IrregFdata {
90            offsets,
91            argvals,
92            values,
93            rangeval: [range_min, range_max],
94        }
95    }
96
97    /// Create from flattened representation (for R interop).
98    ///
99    /// # Arguments
100    /// * `offsets` - Start indices (length n+1)
101    /// * `argvals` - All observation points concatenated
102    /// * `values` - All values concatenated
103    /// * `rangeval` - Domain range `[min, max]`
104    pub fn from_flat(
105        offsets: Vec<usize>,
106        argvals: Vec<f64>,
107        values: Vec<f64>,
108        rangeval: [f64; 2],
109    ) -> Self {
110        IrregFdata {
111            offsets,
112            argvals,
113            values,
114            rangeval,
115        }
116    }
117
118    /// Number of observations.
119    #[inline]
120    pub fn n_obs(&self) -> usize {
121        self.offsets.len().saturating_sub(1)
122    }
123
124    /// Number of points for observation i.
125    #[inline]
126    pub fn n_points(&self, i: usize) -> usize {
127        self.offsets[i + 1] - self.offsets[i]
128    }
129
130    /// Get observation i as a pair of slices (argvals, values).
131    #[inline]
132    pub fn get_obs(&self, i: usize) -> (&[f64], &[f64]) {
133        let start = self.offsets[i];
134        let end = self.offsets[i + 1];
135        (&self.argvals[start..end], &self.values[start..end])
136    }
137
138    /// Total number of observation points across all curves.
139    #[inline]
140    pub fn total_points(&self) -> usize {
141        self.argvals.len()
142    }
143
144    /// Get observation counts for all curves.
145    pub fn obs_counts(&self) -> Vec<usize> {
146        (0..self.n_obs()).map(|i| self.n_points(i)).collect()
147    }
148
149    /// Get minimum number of observations per curve.
150    pub fn min_obs(&self) -> usize {
151        (0..self.n_obs())
152            .map(|i| self.n_points(i))
153            .min()
154            .unwrap_or(0)
155    }
156
157    /// Get maximum number of observations per curve.
158    pub fn max_obs(&self) -> usize {
159        (0..self.n_obs())
160            .map(|i| self.n_points(i))
161            .max()
162            .unwrap_or(0)
163    }
164}
165
166// =============================================================================
167// Integration
168// =============================================================================
169
170/// Compute integral of each curve using trapezoidal rule.
171///
172/// For curve i with observation points t_1, ..., t_m and values x_1, ..., x_m:
173/// ∫f_i(t)dt ≈ Σ_{j=1}^{m-1} (t_{j+1} - t_j) * (x_{j+1} + x_j) / 2
174///
175/// # Arguments
176/// * `offsets` - Start indices (length n+1)
177/// * `argvals` - All observation points concatenated
178/// * `values` - All values concatenated
179///
180/// # Returns
181/// Vector of integrals, one per curve
182pub fn integrate_irreg(offsets: &[usize], argvals: &[f64], values: &[f64]) -> Vec<f64> {
183    let n = offsets.len() - 1;
184
185    iter_maybe_parallel!(0..n)
186        .map(|i| {
187            let start = offsets[i];
188            let end = offsets[i + 1];
189            let t = &argvals[start..end];
190            let x = &values[start..end];
191
192            if t.len() < 2 {
193                return 0.0;
194            }
195
196            let mut integral = 0.0;
197            for j in 1..t.len() {
198                let h = t[j] - t[j - 1];
199                integral += 0.5 * h * (x[j] + x[j - 1]);
200            }
201            integral
202        })
203        .collect()
204}
205
206/// Compute integral for IrregFdata struct.
207pub fn integrate_irreg_struct(ifd: &IrregFdata) -> Vec<f64> {
208    integrate_irreg(&ifd.offsets, &ifd.argvals, &ifd.values)
209}
210
211// =============================================================================
212// Norms
213// =============================================================================
214
215/// Compute Lp norm for each curve in irregular functional data.
216///
217/// ||f_i||_p = (∫|f_i(t)|^p dt)^{1/p}
218///
219/// Uses trapezoidal rule for integration.
220///
221/// # Arguments
222/// * `offsets` - Start indices (length n+1)
223/// * `argvals` - All observation points concatenated
224/// * `values` - All values concatenated
225/// * `p` - Norm order (p >= 1)
226///
227/// # Returns
228/// Vector of norms, one per curve
229pub fn norm_lp_irreg(offsets: &[usize], argvals: &[f64], values: &[f64], p: f64) -> Vec<f64> {
230    let n = offsets.len() - 1;
231
232    iter_maybe_parallel!(0..n)
233        .map(|i| {
234            let start = offsets[i];
235            let end = offsets[i + 1];
236            let t = &argvals[start..end];
237            let x = &values[start..end];
238
239            if t.len() < 2 {
240                return 0.0;
241            }
242
243            let mut integral = 0.0;
244            for j in 1..t.len() {
245                let h = t[j] - t[j - 1];
246                let val_left = x[j - 1].abs().powf(p);
247                let val_right = x[j].abs().powf(p);
248                integral += 0.5 * h * (val_left + val_right);
249            }
250            integral.powf(1.0 / p)
251        })
252        .collect()
253}
254
255// =============================================================================
256// Mean Estimation
257// =============================================================================
258
259/// Epanechnikov kernel function.
260#[inline]
261fn kernel_epanechnikov(u: f64) -> f64 {
262    if u.abs() <= 1.0 {
263        0.75 * (1.0 - u * u)
264    } else {
265        0.0
266    }
267}
268
269/// Gaussian kernel function.
270#[inline]
271fn kernel_gaussian(u: f64) -> f64 {
272    (-0.5 * u * u).exp() / (2.0 * std::f64::consts::PI).sqrt()
273}
274
275/// Estimate mean function at specified target points using kernel smoothing.
276///
277/// Uses local weighted averaging (Nadaraya-Watson estimator) at each target point:
278/// μ̂(t) = Σ_{i,j} K_h(t - t_{ij}) x_{ij} / Σ_{i,j} K_h(t - t_{ij})
279///
280/// # Arguments
281/// * `offsets` - Start indices (length n+1)
282/// * `argvals` - All observation points concatenated
283/// * `values` - All values concatenated
284/// * `target_argvals` - Points at which to estimate the mean
285/// * `bandwidth` - Kernel bandwidth
286/// * `kernel_type` - 0 for Epanechnikov, 1 for Gaussian
287///
288/// # Returns
289/// Estimated mean function values at target points
290pub fn mean_irreg(
291    offsets: &[usize],
292    argvals: &[f64],
293    values: &[f64],
294    target_argvals: &[f64],
295    bandwidth: f64,
296    kernel_type: i32,
297) -> Vec<f64> {
298    let n = offsets.len() - 1;
299    let kernel = match kernel_type {
300        0 => kernel_epanechnikov,
301        _ => kernel_gaussian,
302    };
303
304    slice_maybe_parallel!(target_argvals)
305        .map(|&t| {
306            let mut sum_weights = 0.0;
307            let mut sum_values = 0.0;
308
309            for i in 0..n {
310                let start = offsets[i];
311                let end = offsets[i + 1];
312                let obs_t = &argvals[start..end];
313                let obs_x = &values[start..end];
314
315                for (&ti, &xi) in obs_t.iter().zip(obs_x.iter()) {
316                    let u = (ti - t) / bandwidth;
317                    let w = kernel(u);
318                    sum_weights += w;
319                    sum_values += w * xi;
320                }
321            }
322
323            if sum_weights > 0.0 {
324                sum_values / sum_weights
325            } else {
326                f64::NAN
327            }
328        })
329        .collect()
330}
331
332// =============================================================================
333// Covariance Estimation
334// =============================================================================
335
336/// Estimate covariance at a grid of points using local linear smoothing.
337///
338/// # Arguments
339/// * `offsets` - Start indices (length n+1)
340/// * `argvals` - All observation points concatenated
341/// * `values` - All values concatenated
342/// * `mean_vals` - Pre-computed mean function at observation points (optional)
343/// * `s_grid` - First grid points for covariance
344/// * `t_grid` - Second grid points for covariance
345/// * `bandwidth` - Kernel bandwidth
346///
347/// # Returns
348/// Covariance matrix estimate at (s_grid, t_grid) points
349pub fn cov_irreg(
350    offsets: &[usize],
351    argvals: &[f64],
352    values: &[f64],
353    s_grid: &[f64],
354    t_grid: &[f64],
355    bandwidth: f64,
356) -> Vec<f64> {
357    let n = offsets.len() - 1;
358    let ns = s_grid.len();
359    let nt = t_grid.len();
360
361    // First estimate mean at all observation points
362    let mean_at_obs = mean_irreg(offsets, argvals, values, argvals, bandwidth, 1);
363
364    // Centered values
365    let centered: Vec<f64> = values
366        .iter()
367        .zip(mean_at_obs.iter())
368        .map(|(&v, &m)| v - m)
369        .collect();
370
371    // Estimate covariance at each (s, t) pair
372    let mut cov = vec![0.0; ns * nt];
373
374    for (si, &s) in s_grid.iter().enumerate() {
375        for (ti, &t) in t_grid.iter().enumerate() {
376            let mut sum_weights = 0.0;
377            let mut sum_products = 0.0;
378
379            // Sum over all pairs of observations within each curve
380            for i in 0..n {
381                let start = offsets[i];
382                let end = offsets[i + 1];
383                let obs_t = &argvals[start..end];
384                let obs_c = &centered[start..end];
385
386                for j1 in 0..obs_t.len() {
387                    for j2 in 0..obs_t.len() {
388                        let u1 = (obs_t[j1] - s) / bandwidth;
389                        let u2 = (obs_t[j2] - t) / bandwidth;
390
391                        let w1 = kernel_gaussian(u1);
392                        let w2 = kernel_gaussian(u2);
393                        let w = w1 * w2;
394
395                        sum_weights += w;
396                        sum_products += w * obs_c[j1] * obs_c[j2];
397                    }
398                }
399            }
400
401            if sum_weights > 0.0 {
402                cov[si + ti * ns] = sum_products / sum_weights;
403            }
404        }
405    }
406
407    cov
408}
409
410// =============================================================================
411// Distance Metrics
412// =============================================================================
413
414/// Compute Lp distance between two irregular curves.
415///
416/// Uses the union of observation points and linear interpolation.
417fn lp_distance_pair(t1: &[f64], x1: &[f64], t2: &[f64], x2: &[f64], p: f64) -> f64 {
418    // Create union of time points
419    let mut all_t: Vec<f64> = t1.iter().chain(t2.iter()).copied().collect();
420    all_t.sort_by(|a, b| a.partial_cmp(b).unwrap());
421    all_t.dedup();
422
423    // Filter to common range
424    let t_min = t1.first().unwrap().max(*t2.first().unwrap());
425    let t_max = t1.last().unwrap().min(*t2.last().unwrap());
426
427    let common_t: Vec<f64> = all_t
428        .into_iter()
429        .filter(|&t| t >= t_min && t <= t_max)
430        .collect();
431
432    if common_t.len() < 2 {
433        return f64::NAN;
434    }
435
436    // Interpolate both curves at common points
437    let y1: Vec<f64> = common_t.iter().map(|&t| linear_interp(t1, x1, t)).collect();
438    let y2: Vec<f64> = common_t.iter().map(|&t| linear_interp(t2, x2, t)).collect();
439
440    // Compute integral of |y1 - y2|^p
441    let mut integral = 0.0;
442    for j in 1..common_t.len() {
443        let h = common_t[j] - common_t[j - 1];
444        let val_left = (y1[j - 1] - y2[j - 1]).abs().powf(p);
445        let val_right = (y1[j] - y2[j]).abs().powf(p);
446        integral += 0.5 * h * (val_left + val_right);
447    }
448
449    integral.powf(1.0 / p)
450}
451
452/// Linear interpolation at point t.
453fn linear_interp(argvals: &[f64], values: &[f64], t: f64) -> f64 {
454    if t <= argvals[0] {
455        return values[0];
456    }
457    if t >= argvals[argvals.len() - 1] {
458        return values[values.len() - 1];
459    }
460
461    // Find the interval
462    let idx = argvals.iter().position(|&x| x > t).unwrap();
463    let t0 = argvals[idx - 1];
464    let t1 = argvals[idx];
465    let x0 = values[idx - 1];
466    let x1 = values[idx];
467
468    // Linear interpolation
469    x0 + (x1 - x0) * (t - t0) / (t1 - t0)
470}
471
472/// Compute pairwise Lp distances for irregular functional data.
473///
474/// # Arguments
475/// * `offsets` - Start indices (length n+1)
476/// * `argvals` - All observation points concatenated
477/// * `values` - All values concatenated
478/// * `p` - Norm order
479///
480/// # Returns
481/// Distance matrix (n × n) in column-major format
482pub fn metric_lp_irreg(offsets: &[usize], argvals: &[f64], values: &[f64], p: f64) -> Vec<f64> {
483    let n = offsets.len() - 1;
484    let mut dist = vec![0.0; n * n];
485
486    // Compute upper triangle in parallel
487    let pairs: Vec<(usize, usize)> = (0..n)
488        .flat_map(|i| (i + 1..n).map(move |j| (i, j)))
489        .collect();
490
491    let distances: Vec<f64> = slice_maybe_parallel!(pairs)
492        .map(|&(i, j)| {
493            let start_i = offsets[i];
494            let end_i = offsets[i + 1];
495            let start_j = offsets[j];
496            let end_j = offsets[j + 1];
497
498            lp_distance_pair(
499                &argvals[start_i..end_i],
500                &values[start_i..end_i],
501                &argvals[start_j..end_j],
502                &values[start_j..end_j],
503                p,
504            )
505        })
506        .collect();
507
508    // Fill symmetric matrix
509    for (k, &(i, j)) in pairs.iter().enumerate() {
510        dist[i + j * n] = distances[k];
511        dist[j + i * n] = distances[k];
512    }
513
514    dist
515}
516
517// =============================================================================
518// Conversion to Regular Grid
519// =============================================================================
520
521/// Convert irregular data to regular grid via linear interpolation.
522///
523/// Missing values (outside observation range) are marked as NaN.
524///
525/// # Arguments
526/// * `offsets` - Start indices (length n+1)
527/// * `argvals` - All observation points concatenated
528/// * `values` - All values concatenated
529/// * `target_grid` - Regular grid to interpolate to
530///
531/// # Returns
532/// Data matrix (n × len(target_grid)) in column-major format
533pub fn to_regular_grid(
534    offsets: &[usize],
535    argvals: &[f64],
536    values: &[f64],
537    target_grid: &[f64],
538) -> Vec<f64> {
539    let n = offsets.len() - 1;
540    let m = target_grid.len();
541
542    iter_maybe_parallel!(0..n)
543        .flat_map(|i| {
544            let start = offsets[i];
545            let end = offsets[i + 1];
546            let obs_t = &argvals[start..end];
547            let obs_x = &values[start..end];
548
549            target_grid
550                .iter()
551                .map(|&t| {
552                    if obs_t.is_empty() || t < obs_t[0] || t > obs_t[obs_t.len() - 1] {
553                        f64::NAN
554                    } else {
555                        linear_interp(obs_t, obs_x, t)
556                    }
557                })
558                .collect::<Vec<f64>>()
559        })
560        .collect::<Vec<f64>>()
561        // Transpose to column-major
562        .chunks(m)
563        .enumerate()
564        .fold(vec![0.0; n * m], |mut acc, (i, row)| {
565            for (j, &val) in row.iter().enumerate() {
566                acc[i + j * n] = val;
567            }
568            acc
569        })
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn test_from_lists() {
578        let argvals_list = vec![vec![0.0, 0.5, 1.0], vec![0.0, 1.0]];
579        let values_list = vec![vec![1.0, 2.0, 3.0], vec![1.0, 3.0]];
580
581        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
582
583        assert_eq!(ifd.n_obs(), 2);
584        assert_eq!(ifd.n_points(0), 3);
585        assert_eq!(ifd.n_points(1), 2);
586        assert_eq!(ifd.total_points(), 5);
587    }
588
589    #[test]
590    fn test_get_obs() {
591        let argvals_list = vec![vec![0.0, 0.5, 1.0], vec![0.0, 1.0]];
592        let values_list = vec![vec![1.0, 2.0, 3.0], vec![1.0, 3.0]];
593
594        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
595
596        let (t0, x0) = ifd.get_obs(0);
597        assert_eq!(t0, &[0.0, 0.5, 1.0]);
598        assert_eq!(x0, &[1.0, 2.0, 3.0]);
599
600        let (t1, x1) = ifd.get_obs(1);
601        assert_eq!(t1, &[0.0, 1.0]);
602        assert_eq!(x1, &[1.0, 3.0]);
603    }
604
605    #[test]
606    fn test_integrate_irreg() {
607        // Integrate constant function = 1 over [0, 1]
608        let offsets = vec![0, 3, 6];
609        let argvals = vec![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
610        let values = vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
611
612        let integrals = integrate_irreg(&offsets, &argvals, &values);
613
614        assert!((integrals[0] - 1.0).abs() < 1e-10);
615        assert!((integrals[1] - 2.0).abs() < 1e-10);
616    }
617
618    #[test]
619    fn test_norm_lp_irreg() {
620        // L2 norm of constant function = c is c (on \[0,1\])
621        let offsets = vec![0, 3];
622        let argvals = vec![0.0, 0.5, 1.0];
623        let values = vec![2.0, 2.0, 2.0];
624
625        let norms = norm_lp_irreg(&offsets, &argvals, &values, 2.0);
626
627        assert!((norms[0] - 2.0).abs() < 1e-10);
628    }
629
630    #[test]
631    fn test_linear_interp() {
632        let t = vec![0.0, 1.0, 2.0];
633        let x = vec![0.0, 2.0, 4.0];
634
635        assert!((linear_interp(&t, &x, 0.5) - 1.0).abs() < 1e-10);
636        assert!((linear_interp(&t, &x, 1.5) - 3.0).abs() < 1e-10);
637    }
638
639    #[test]
640    fn test_mean_irreg() {
641        // Two identical curves should give exact mean
642        let offsets = vec![0, 3, 6];
643        let argvals = vec![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
644        let values = vec![0.0, 1.0, 2.0, 0.0, 1.0, 2.0];
645
646        let target = vec![0.0, 0.5, 1.0];
647        let mean = mean_irreg(&offsets, &argvals, &values, &target, 0.5, 1);
648
649        // Mean should be close to the common values
650        assert!((mean[1] - 1.0).abs() < 0.3);
651    }
652
653    // ========================================================================
654    // Tests for from_flat and accessors
655    // ========================================================================
656
657    #[test]
658    fn test_from_flat() {
659        let offsets = vec![0, 3, 5, 10];
660        let argvals = vec![0.0, 0.5, 1.0, 0.0, 1.0, 0.0, 0.2, 0.4, 0.6, 0.8];
661        let values = vec![1.0, 2.0, 3.0, 1.0, 3.0, 0.0, 1.0, 2.0, 3.0, 4.0];
662        let rangeval = [0.0, 1.0];
663
664        let ifd = IrregFdata::from_flat(offsets.clone(), argvals.clone(), values.clone(), rangeval);
665
666        assert_eq!(ifd.n_obs(), 3);
667        assert_eq!(ifd.offsets, offsets);
668        assert_eq!(ifd.argvals, argvals);
669        assert_eq!(ifd.values, values);
670        assert_eq!(ifd.rangeval, rangeval);
671    }
672
673    #[test]
674    fn test_accessors_n_obs_n_points_total() {
675        let argvals_list = vec![
676            vec![0.0, 0.5, 1.0],             // 3 points
677            vec![0.0, 1.0],                  // 2 points
678            vec![0.0, 0.25, 0.5, 0.75, 1.0], // 5 points
679        ];
680        let values_list = vec![
681            vec![1.0, 2.0, 3.0],
682            vec![1.0, 3.0],
683            vec![0.0, 1.0, 2.0, 3.0, 4.0],
684        ];
685
686        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
687
688        // Test n_obs
689        assert_eq!(ifd.n_obs(), 3);
690
691        // Test n_points for each curve
692        assert_eq!(ifd.n_points(0), 3);
693        assert_eq!(ifd.n_points(1), 2);
694        assert_eq!(ifd.n_points(2), 5);
695
696        // Test total_points
697        assert_eq!(ifd.total_points(), 10);
698    }
699
700    #[test]
701    fn test_obs_counts() {
702        let argvals_list = vec![
703            vec![0.0, 0.5, 1.0],             // 3 points
704            vec![0.0, 1.0],                  // 2 points
705            vec![0.0, 0.25, 0.5, 0.75, 1.0], // 5 points
706        ];
707        let values_list = vec![
708            vec![1.0, 2.0, 3.0],
709            vec![1.0, 3.0],
710            vec![0.0, 1.0, 2.0, 3.0, 4.0],
711        ];
712
713        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
714        let counts = ifd.obs_counts();
715
716        assert_eq!(counts, vec![3, 2, 5]);
717    }
718
719    #[test]
720    fn test_min_max_obs() {
721        let argvals_list = vec![
722            vec![0.0, 0.5, 1.0],             // 3 points
723            vec![0.0, 1.0],                  // 2 points
724            vec![0.0, 0.25, 0.5, 0.75, 1.0], // 5 points
725        ];
726        let values_list = vec![
727            vec![1.0, 2.0, 3.0],
728            vec![1.0, 3.0],
729            vec![0.0, 1.0, 2.0, 3.0, 4.0],
730        ];
731
732        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
733
734        assert_eq!(ifd.min_obs(), 2);
735        assert_eq!(ifd.max_obs(), 5);
736    }
737
738    #[test]
739    fn test_min_max_obs_empty() {
740        let ifd = IrregFdata::from_lists(&[], &[]);
741        assert_eq!(ifd.min_obs(), 0);
742        assert_eq!(ifd.max_obs(), 0);
743    }
744
745    // ========================================================================
746    // Tests for cov_irreg
747    // ========================================================================
748
749    #[test]
750    fn test_cov_irreg_identical_curves() {
751        // Two identical curves should have zero covariance (no variability)
752        let offsets = vec![0, 5, 10];
753        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
754        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 2.0, 1.0];
755
756        let grid = vec![0.25, 0.5, 0.75];
757        let cov = cov_irreg(&offsets, &argvals, &values, &grid, &grid, 0.3);
758
759        // Covariance should be close to 0 (identical curves)
760        assert_eq!(cov.len(), 9);
761        // Diagonal should be variance (close to 0 for identical curves)
762        for i in 0..3 {
763            assert!(
764                cov[i + i * 3].abs() < 0.5,
765                "Diagonal cov[{},{}] = {} should be near 0",
766                i,
767                i,
768                cov[i + i * 3]
769            );
770        }
771    }
772
773    #[test]
774    fn test_cov_irreg_symmetry() {
775        // Covariance matrix should be symmetric
776        let offsets = vec![0, 5, 10, 15];
777        let argvals = vec![
778            0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0,
779        ];
780        let values = vec![
781            1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 4.0, 1.0, 0.0, 2.0, 3.0, 2.0, 3.0, 2.0,
782        ];
783
784        let grid = vec![0.25, 0.5, 0.75];
785        let cov = cov_irreg(&offsets, &argvals, &values, &grid, &grid, 0.3);
786
787        // Check symmetry: cov[i,j] = cov[j,i]
788        for i in 0..3 {
789            for j in 0..3 {
790                assert!(
791                    (cov[i + j * 3] - cov[j + i * 3]).abs() < 1e-10,
792                    "Cov[{},{}] = {} != Cov[{},{}] = {}",
793                    i,
794                    j,
795                    cov[i + j * 3],
796                    j,
797                    i,
798                    cov[j + i * 3]
799                );
800            }
801        }
802    }
803
804    #[test]
805    fn test_cov_irreg_diagonal_positive() {
806        // Diagonal (variances) should be non-negative
807        let offsets = vec![0, 5, 10, 15];
808        let argvals = vec![
809            0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0,
810        ];
811        let values = vec![
812            1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 4.0, 1.0, 0.0, 2.0, 3.0, 2.0, 3.0, 2.0,
813        ];
814
815        let grid = vec![0.25, 0.5, 0.75];
816        let cov = cov_irreg(&offsets, &argvals, &values, &grid, &grid, 0.3);
817
818        for i in 0..3 {
819            assert!(
820                cov[i + i * 3] >= -1e-10,
821                "Variance at {} should be non-negative: {}",
822                i,
823                cov[i + i * 3]
824            );
825        }
826    }
827
828    #[test]
829    fn test_cov_irreg_different_grids() {
830        let offsets = vec![0, 5, 10];
831        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
832        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0];
833
834        let s_grid = vec![0.25, 0.5];
835        let t_grid = vec![0.5, 0.75];
836        let cov = cov_irreg(&offsets, &argvals, &values, &s_grid, &t_grid, 0.3);
837
838        // Should produce a 2x2 matrix
839        assert_eq!(cov.len(), 4);
840    }
841
842    // ========================================================================
843    // Tests for metric_lp_irreg
844    // ========================================================================
845
846    #[test]
847    fn test_metric_lp_irreg_self_distance_zero() {
848        // Distance to self should be 0
849        let offsets = vec![0, 5, 10];
850        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
851        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0];
852
853        let dist = metric_lp_irreg(&offsets, &argvals, &values, 2.0);
854
855        // Diagonal should be 0
856        let n = 2;
857        for i in 0..n {
858            assert!(
859                dist[i + i * n].abs() < 1e-10,
860                "Self-distance d[{},{}] = {} should be 0",
861                i,
862                i,
863                dist[i + i * n]
864            );
865        }
866    }
867
868    #[test]
869    fn test_metric_lp_irreg_symmetry() {
870        // Distance matrix should be symmetric
871        let offsets = vec![0, 5, 10, 15];
872        let argvals = vec![
873            0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0,
874        ];
875        let values = vec![
876            1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 2.0, 3.0, 4.0, 3.0, 2.0,
877        ];
878
879        let dist = metric_lp_irreg(&offsets, &argvals, &values, 2.0);
880        let n = 3;
881
882        for i in 0..n {
883            for j in 0..n {
884                assert!(
885                    (dist[i + j * n] - dist[j + i * n]).abs() < 1e-10,
886                    "Dist[{},{}] = {} != Dist[{},{}] = {}",
887                    i,
888                    j,
889                    dist[i + j * n],
890                    j,
891                    i,
892                    dist[j + i * n]
893                );
894            }
895        }
896    }
897
898    #[test]
899    fn test_metric_lp_irreg_triangle_inequality() {
900        // Triangle inequality: d(a,c) <= d(a,b) + d(b,c)
901        let offsets = vec![0, 5, 10, 15];
902        let argvals = vec![
903            0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0,
904        ];
905        let values = vec![
906            0.0, 0.0, 0.0, 0.0, 0.0, // curve a
907            1.0, 1.0, 1.0, 1.0, 1.0, // curve b
908            2.0, 2.0, 2.0, 2.0, 2.0, // curve c
909        ];
910
911        let dist = metric_lp_irreg(&offsets, &argvals, &values, 2.0);
912        let n = 3;
913
914        // d(a,c) <= d(a,b) + d(b,c)
915        let d_ac = dist[2 * n];
916        let d_ab = dist[n];
917        let d_bc = dist[1 + 2 * n];
918
919        assert!(
920            d_ac <= d_ab + d_bc + 1e-10,
921            "Triangle inequality violated: {} > {} + {}",
922            d_ac,
923            d_ab,
924            d_bc
925        );
926    }
927
928    // ========================================================================
929    // Tests for to_regular_grid
930    // ========================================================================
931
932    #[test]
933    fn test_to_regular_grid_basic() {
934        let offsets = vec![0, 5];
935        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
936        let values = vec![0.0, 1.0, 2.0, 3.0, 4.0];
937
938        let grid = vec![0.0, 0.5, 1.0];
939        let result = to_regular_grid(&offsets, &argvals, &values, &grid);
940
941        // Should produce 1 curve x 3 points
942        assert_eq!(result.len(), 3);
943
944        // Check interpolated values
945        assert!((result[0] - 0.0).abs() < 1e-10, "At t=0: {}", result[0]);
946        assert!((result[1] - 2.0).abs() < 1e-10, "At t=0.5: {}", result[1]);
947        assert!((result[2] - 4.0).abs() < 1e-10, "At t=1: {}", result[2]);
948    }
949
950    #[test]
951    fn test_to_regular_grid_multiple_curves() {
952        let offsets = vec![0, 5, 10];
953        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
954        let values = vec![
955            0.0, 1.0, 2.0, 3.0, 4.0, // Linear: y = 4t
956            4.0, 3.0, 2.0, 1.0, 0.0, // Linear: y = 4 - 4t
957        ];
958
959        let grid = vec![0.0, 0.5, 1.0];
960        let result = to_regular_grid(&offsets, &argvals, &values, &grid);
961
962        // Should produce 2 curves x 3 points = 6 values in column-major
963        assert_eq!(result.len(), 6);
964
965        // Curve 0 at t=0.5 should be 2.0
966        assert!((result[2] - 2.0).abs() < 1e-10);
967        // Curve 1 at t=0.5 should be 2.0
968        assert!((result[1 + 2] - 2.0).abs() < 1e-10);
969    }
970
971    #[test]
972    fn test_to_regular_grid_boundary_nan() {
973        let offsets = vec![0, 3];
974        let argvals = vec![0.2, 0.5, 0.8]; // Curve only defined on [0.2, 0.8]
975        let values = vec![1.0, 2.0, 3.0];
976
977        let grid = vec![0.0, 0.5, 1.0]; // Grid extends beyond curve range
978        let result = to_regular_grid(&offsets, &argvals, &values, &grid);
979
980        // At t=0.0 (before curve starts), should be NaN
981        assert!(result[0].is_nan(), "t=0 should be NaN");
982        // At t=0.5 (within range), should be valid
983        assert!((result[1] - 2.0).abs() < 1e-10, "t=0.5: {}", result[1]);
984        // At t=1.0 (after curve ends), should be NaN
985        assert!(result[2].is_nan(), "t=1 should be NaN");
986    }
987}