Skip to main content

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            cov[si + ti * ns] =
377                accumulate_cov_at_point(offsets, argvals, &centered, n, s, t, bandwidth);
378        }
379    }
380
381    cov
382}
383
384/// Compute kernel-weighted covariance estimate at a single (s, t) grid point.
385fn accumulate_cov_at_point(
386    offsets: &[usize],
387    obs_times: &[f64],
388    centered: &[f64],
389    n: usize,
390    s: f64,
391    t: f64,
392    bandwidth: f64,
393) -> f64 {
394    let mut sum_weights = 0.0;
395    let mut sum_products = 0.0;
396
397    for i in 0..n {
398        let start = offsets[i];
399        let end = offsets[i + 1];
400        let obs_t = &obs_times[start..end];
401        let obs_c = &centered[start..end];
402
403        for j1 in 0..obs_t.len() {
404            for j2 in 0..obs_t.len() {
405                let w1 = kernel_gaussian((obs_t[j1] - s) / bandwidth);
406                let w2 = kernel_gaussian((obs_t[j2] - t) / bandwidth);
407                let w = w1 * w2;
408
409                sum_weights += w;
410                sum_products += w * obs_c[j1] * obs_c[j2];
411            }
412        }
413    }
414
415    if sum_weights > 0.0 {
416        sum_products / sum_weights
417    } else {
418        0.0
419    }
420}
421
422// =============================================================================
423// Distance Metrics
424// =============================================================================
425
426/// Compute Lp distance between two irregular curves.
427///
428/// Uses the union of observation points and linear interpolation.
429fn lp_distance_pair(t1: &[f64], x1: &[f64], t2: &[f64], x2: &[f64], p: f64) -> f64 {
430    // Create union of time points
431    let mut all_t: Vec<f64> = t1.iter().chain(t2.iter()).copied().collect();
432    all_t.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
433    all_t.dedup();
434
435    // Filter to common range
436    let (t_min, t_max) = match (t1.first(), t1.last(), t2.first(), t2.last()) {
437        (Some(&a), Some(&b), Some(&c), Some(&d)) => (a.max(c), b.min(d)),
438        _ => return f64::NAN,
439    };
440
441    let common_t: Vec<f64> = all_t
442        .into_iter()
443        .filter(|&t| t >= t_min && t <= t_max)
444        .collect();
445
446    if common_t.len() < 2 {
447        return f64::NAN;
448    }
449
450    // Interpolate both curves at common points
451    let y1: Vec<f64> = common_t.iter().map(|&t| linear_interp(t1, x1, t)).collect();
452    let y2: Vec<f64> = common_t.iter().map(|&t| linear_interp(t2, x2, t)).collect();
453
454    // Compute integral of |y1 - y2|^p
455    let mut integral = 0.0;
456    for j in 1..common_t.len() {
457        let h = common_t[j] - common_t[j - 1];
458        let val_left = (y1[j - 1] - y2[j - 1]).abs().powf(p);
459        let val_right = (y1[j] - y2[j]).abs().powf(p);
460        integral += 0.5 * h * (val_left + val_right);
461    }
462
463    integral.powf(1.0 / p)
464}
465
466/// Linear interpolation at point t.
467fn linear_interp(argvals: &[f64], values: &[f64], t: f64) -> f64 {
468    if t <= argvals[0] {
469        return values[0];
470    }
471    if t >= argvals[argvals.len() - 1] {
472        return values[values.len() - 1];
473    }
474
475    // Find the interval
476    let idx = argvals.iter().position(|&x| x > t).unwrap();
477    let t0 = argvals[idx - 1];
478    let t1 = argvals[idx];
479    let x0 = values[idx - 1];
480    let x1 = values[idx];
481
482    // Linear interpolation
483    x0 + (x1 - x0) * (t - t0) / (t1 - t0)
484}
485
486/// Compute pairwise Lp distances for irregular functional data.
487///
488/// # Arguments
489/// * `offsets` - Start indices (length n+1)
490/// * `argvals` - All observation points concatenated
491/// * `values` - All values concatenated
492/// * `p` - Norm order
493///
494/// # Returns
495/// Distance matrix (n × n) in column-major format
496pub fn metric_lp_irreg(offsets: &[usize], argvals: &[f64], values: &[f64], p: f64) -> Vec<f64> {
497    let n = offsets.len() - 1;
498    let mut dist = vec![0.0; n * n];
499
500    // Compute upper triangle in parallel
501    let pairs: Vec<(usize, usize)> = (0..n)
502        .flat_map(|i| (i + 1..n).map(move |j| (i, j)))
503        .collect();
504
505    let distances: Vec<f64> = slice_maybe_parallel!(pairs)
506        .map(|&(i, j)| {
507            let start_i = offsets[i];
508            let end_i = offsets[i + 1];
509            let start_j = offsets[j];
510            let end_j = offsets[j + 1];
511
512            lp_distance_pair(
513                &argvals[start_i..end_i],
514                &values[start_i..end_i],
515                &argvals[start_j..end_j],
516                &values[start_j..end_j],
517                p,
518            )
519        })
520        .collect();
521
522    // Fill symmetric matrix
523    for (k, &(i, j)) in pairs.iter().enumerate() {
524        dist[i + j * n] = distances[k];
525        dist[j + i * n] = distances[k];
526    }
527
528    dist
529}
530
531// =============================================================================
532// Conversion to Regular Grid
533// =============================================================================
534
535/// Convert irregular data to regular grid via linear interpolation.
536///
537/// Missing values (outside observation range) are marked as NaN.
538///
539/// # Arguments
540/// * `offsets` - Start indices (length n+1)
541/// * `argvals` - All observation points concatenated
542/// * `values` - All values concatenated
543/// * `target_grid` - Regular grid to interpolate to
544///
545/// # Returns
546/// Data matrix (n × len(target_grid)) in column-major format
547pub fn to_regular_grid(
548    offsets: &[usize],
549    argvals: &[f64],
550    values: &[f64],
551    target_grid: &[f64],
552) -> Vec<f64> {
553    let n = offsets.len() - 1;
554    let m = target_grid.len();
555
556    iter_maybe_parallel!(0..n)
557        .flat_map(|i| {
558            let start = offsets[i];
559            let end = offsets[i + 1];
560            let obs_t = &argvals[start..end];
561            let obs_x = &values[start..end];
562
563            target_grid
564                .iter()
565                .map(|&t| {
566                    if obs_t.is_empty() || t < obs_t[0] || t > obs_t[obs_t.len() - 1] {
567                        f64::NAN
568                    } else {
569                        linear_interp(obs_t, obs_x, t)
570                    }
571                })
572                .collect::<Vec<f64>>()
573        })
574        .collect::<Vec<f64>>()
575        // Transpose to column-major
576        .chunks(m)
577        .enumerate()
578        .fold(vec![0.0; n * m], |mut acc, (i, row)| {
579            for (j, &val) in row.iter().enumerate() {
580                acc[i + j * n] = val;
581            }
582            acc
583        })
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    #[test]
591    fn test_from_lists() {
592        let argvals_list = vec![vec![0.0, 0.5, 1.0], vec![0.0, 1.0]];
593        let values_list = vec![vec![1.0, 2.0, 3.0], vec![1.0, 3.0]];
594
595        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
596
597        assert_eq!(ifd.n_obs(), 2);
598        assert_eq!(ifd.n_points(0), 3);
599        assert_eq!(ifd.n_points(1), 2);
600        assert_eq!(ifd.total_points(), 5);
601    }
602
603    #[test]
604    fn test_get_obs() {
605        let argvals_list = vec![vec![0.0, 0.5, 1.0], vec![0.0, 1.0]];
606        let values_list = vec![vec![1.0, 2.0, 3.0], vec![1.0, 3.0]];
607
608        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
609
610        let (t0, x0) = ifd.get_obs(0);
611        assert_eq!(t0, &[0.0, 0.5, 1.0]);
612        assert_eq!(x0, &[1.0, 2.0, 3.0]);
613
614        let (t1, x1) = ifd.get_obs(1);
615        assert_eq!(t1, &[0.0, 1.0]);
616        assert_eq!(x1, &[1.0, 3.0]);
617    }
618
619    #[test]
620    fn test_integrate_irreg() {
621        // Integrate constant function = 1 over [0, 1]
622        let offsets = vec![0, 3, 6];
623        let argvals = vec![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
624        let values = vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
625
626        let integrals = integrate_irreg(&offsets, &argvals, &values);
627
628        assert!((integrals[0] - 1.0).abs() < 1e-10);
629        assert!((integrals[1] - 2.0).abs() < 1e-10);
630    }
631
632    #[test]
633    fn test_norm_lp_irreg() {
634        // L2 norm of constant function = c is c (on \[0,1\])
635        let offsets = vec![0, 3];
636        let argvals = vec![0.0, 0.5, 1.0];
637        let values = vec![2.0, 2.0, 2.0];
638
639        let norms = norm_lp_irreg(&offsets, &argvals, &values, 2.0);
640
641        assert!((norms[0] - 2.0).abs() < 1e-10);
642    }
643
644    #[test]
645    fn test_linear_interp() {
646        let t = vec![0.0, 1.0, 2.0];
647        let x = vec![0.0, 2.0, 4.0];
648
649        assert!((linear_interp(&t, &x, 0.5) - 1.0).abs() < 1e-10);
650        assert!((linear_interp(&t, &x, 1.5) - 3.0).abs() < 1e-10);
651    }
652
653    #[test]
654    fn test_mean_irreg() {
655        // Two identical curves should give exact mean
656        let offsets = vec![0, 3, 6];
657        let argvals = vec![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
658        let values = vec![0.0, 1.0, 2.0, 0.0, 1.0, 2.0];
659
660        let target = vec![0.0, 0.5, 1.0];
661        let mean = mean_irreg(&offsets, &argvals, &values, &target, 0.5, 1);
662
663        // Mean should be close to the common values
664        assert!((mean[1] - 1.0).abs() < 0.3);
665    }
666
667    // ========================================================================
668    // Tests for from_flat and accessors
669    // ========================================================================
670
671    #[test]
672    fn test_from_flat() {
673        let offsets = vec![0, 3, 5, 10];
674        let argvals = vec![0.0, 0.5, 1.0, 0.0, 1.0, 0.0, 0.2, 0.4, 0.6, 0.8];
675        let values = vec![1.0, 2.0, 3.0, 1.0, 3.0, 0.0, 1.0, 2.0, 3.0, 4.0];
676        let rangeval = [0.0, 1.0];
677
678        let ifd = IrregFdata::from_flat(offsets.clone(), argvals.clone(), values.clone(), rangeval);
679
680        assert_eq!(ifd.n_obs(), 3);
681        assert_eq!(ifd.offsets, offsets);
682        assert_eq!(ifd.argvals, argvals);
683        assert_eq!(ifd.values, values);
684        assert_eq!(ifd.rangeval, rangeval);
685    }
686
687    #[test]
688    fn test_accessors_n_obs_n_points_total() {
689        let argvals_list = vec![
690            vec![0.0, 0.5, 1.0],             // 3 points
691            vec![0.0, 1.0],                  // 2 points
692            vec![0.0, 0.25, 0.5, 0.75, 1.0], // 5 points
693        ];
694        let values_list = vec![
695            vec![1.0, 2.0, 3.0],
696            vec![1.0, 3.0],
697            vec![0.0, 1.0, 2.0, 3.0, 4.0],
698        ];
699
700        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
701
702        // Test n_obs
703        assert_eq!(ifd.n_obs(), 3);
704
705        // Test n_points for each curve
706        assert_eq!(ifd.n_points(0), 3);
707        assert_eq!(ifd.n_points(1), 2);
708        assert_eq!(ifd.n_points(2), 5);
709
710        // Test total_points
711        assert_eq!(ifd.total_points(), 10);
712    }
713
714    #[test]
715    fn test_obs_counts() {
716        let argvals_list = vec![
717            vec![0.0, 0.5, 1.0],             // 3 points
718            vec![0.0, 1.0],                  // 2 points
719            vec![0.0, 0.25, 0.5, 0.75, 1.0], // 5 points
720        ];
721        let values_list = vec![
722            vec![1.0, 2.0, 3.0],
723            vec![1.0, 3.0],
724            vec![0.0, 1.0, 2.0, 3.0, 4.0],
725        ];
726
727        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
728        let counts = ifd.obs_counts();
729
730        assert_eq!(counts, vec![3, 2, 5]);
731    }
732
733    #[test]
734    fn test_min_max_obs() {
735        let argvals_list = vec![
736            vec![0.0, 0.5, 1.0],             // 3 points
737            vec![0.0, 1.0],                  // 2 points
738            vec![0.0, 0.25, 0.5, 0.75, 1.0], // 5 points
739        ];
740        let values_list = vec![
741            vec![1.0, 2.0, 3.0],
742            vec![1.0, 3.0],
743            vec![0.0, 1.0, 2.0, 3.0, 4.0],
744        ];
745
746        let ifd = IrregFdata::from_lists(&argvals_list, &values_list);
747
748        assert_eq!(ifd.min_obs(), 2);
749        assert_eq!(ifd.max_obs(), 5);
750    }
751
752    #[test]
753    fn test_min_max_obs_empty() {
754        let ifd = IrregFdata::from_lists(&[], &[]);
755        assert_eq!(ifd.min_obs(), 0);
756        assert_eq!(ifd.max_obs(), 0);
757    }
758
759    // ========================================================================
760    // Tests for cov_irreg
761    // ========================================================================
762
763    #[test]
764    fn test_cov_irreg_identical_curves() {
765        // Two identical curves should have zero covariance (no variability)
766        let offsets = vec![0, 5, 10];
767        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
768        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 2.0, 1.0];
769
770        let grid = vec![0.25, 0.5, 0.75];
771        let cov = cov_irreg(&offsets, &argvals, &values, &grid, &grid, 0.3);
772
773        // Covariance should be close to 0 (identical curves)
774        assert_eq!(cov.len(), 9);
775        // Diagonal should be variance (close to 0 for identical curves)
776        for i in 0..3 {
777            assert!(
778                cov[i + i * 3].abs() < 0.5,
779                "Diagonal cov[{},{}] = {} should be near 0",
780                i,
781                i,
782                cov[i + i * 3]
783            );
784        }
785    }
786
787    #[test]
788    fn test_cov_irreg_symmetry() {
789        // Covariance matrix should be symmetric
790        let offsets = vec![0, 5, 10, 15];
791        let argvals = vec![
792            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,
793        ];
794        let values = vec![
795            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,
796        ];
797
798        let grid = vec![0.25, 0.5, 0.75];
799        let cov = cov_irreg(&offsets, &argvals, &values, &grid, &grid, 0.3);
800
801        // Check symmetry: cov[i,j] = cov[j,i]
802        for i in 0..3 {
803            for j in 0..3 {
804                assert!(
805                    (cov[i + j * 3] - cov[j + i * 3]).abs() < 1e-10,
806                    "Cov[{},{}] = {} != Cov[{},{}] = {}",
807                    i,
808                    j,
809                    cov[i + j * 3],
810                    j,
811                    i,
812                    cov[j + i * 3]
813                );
814            }
815        }
816    }
817
818    #[test]
819    fn test_cov_irreg_diagonal_positive() {
820        // Diagonal (variances) should be non-negative
821        let offsets = vec![0, 5, 10, 15];
822        let argvals = vec![
823            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,
824        ];
825        let values = vec![
826            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,
827        ];
828
829        let grid = vec![0.25, 0.5, 0.75];
830        let cov = cov_irreg(&offsets, &argvals, &values, &grid, &grid, 0.3);
831
832        for i in 0..3 {
833            assert!(
834                cov[i + i * 3] >= -1e-10,
835                "Variance at {} should be non-negative: {}",
836                i,
837                cov[i + i * 3]
838            );
839        }
840    }
841
842    #[test]
843    fn test_cov_irreg_different_grids() {
844        let offsets = vec![0, 5, 10];
845        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
846        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0];
847
848        let s_grid = vec![0.25, 0.5];
849        let t_grid = vec![0.5, 0.75];
850        let cov = cov_irreg(&offsets, &argvals, &values, &s_grid, &t_grid, 0.3);
851
852        // Should produce a 2x2 matrix
853        assert_eq!(cov.len(), 4);
854    }
855
856    // ========================================================================
857    // Tests for metric_lp_irreg
858    // ========================================================================
859
860    #[test]
861    fn test_metric_lp_irreg_self_distance_zero() {
862        // Distance to self should be 0
863        let offsets = vec![0, 5, 10];
864        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
865        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0];
866
867        let dist = metric_lp_irreg(&offsets, &argvals, &values, 2.0);
868
869        // Diagonal should be 0
870        let n = 2;
871        for i in 0..n {
872            assert!(
873                dist[i + i * n].abs() < 1e-10,
874                "Self-distance d[{},{}] = {} should be 0",
875                i,
876                i,
877                dist[i + i * n]
878            );
879        }
880    }
881
882    #[test]
883    fn test_metric_lp_irreg_symmetry() {
884        // Distance matrix should be symmetric
885        let offsets = vec![0, 5, 10, 15];
886        let argvals = vec![
887            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,
888        ];
889        let values = vec![
890            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,
891        ];
892
893        let dist = metric_lp_irreg(&offsets, &argvals, &values, 2.0);
894        let n = 3;
895
896        for i in 0..n {
897            for j in 0..n {
898                assert!(
899                    (dist[i + j * n] - dist[j + i * n]).abs() < 1e-10,
900                    "Dist[{},{}] = {} != Dist[{},{}] = {}",
901                    i,
902                    j,
903                    dist[i + j * n],
904                    j,
905                    i,
906                    dist[j + i * n]
907                );
908            }
909        }
910    }
911
912    #[test]
913    fn test_metric_lp_irreg_triangle_inequality() {
914        // Triangle inequality: d(a,c) <= d(a,b) + d(b,c)
915        let offsets = vec![0, 5, 10, 15];
916        let argvals = vec![
917            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,
918        ];
919        let values = vec![
920            0.0, 0.0, 0.0, 0.0, 0.0, // curve a
921            1.0, 1.0, 1.0, 1.0, 1.0, // curve b
922            2.0, 2.0, 2.0, 2.0, 2.0, // curve c
923        ];
924
925        let dist = metric_lp_irreg(&offsets, &argvals, &values, 2.0);
926        let n = 3;
927
928        // d(a,c) <= d(a,b) + d(b,c)
929        let d_ac = dist[2 * n];
930        let d_ab = dist[n];
931        let d_bc = dist[1 + 2 * n];
932
933        assert!(
934            d_ac <= d_ab + d_bc + 1e-10,
935            "Triangle inequality violated: {} > {} + {}",
936            d_ac,
937            d_ab,
938            d_bc
939        );
940    }
941
942    // ========================================================================
943    // Tests for to_regular_grid
944    // ========================================================================
945
946    #[test]
947    fn test_to_regular_grid_basic() {
948        let offsets = vec![0, 5];
949        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
950        let values = vec![0.0, 1.0, 2.0, 3.0, 4.0];
951
952        let grid = vec![0.0, 0.5, 1.0];
953        let result = to_regular_grid(&offsets, &argvals, &values, &grid);
954
955        // Should produce 1 curve x 3 points
956        assert_eq!(result.len(), 3);
957
958        // Check interpolated values
959        assert!((result[0] - 0.0).abs() < 1e-10, "At t=0: {}", result[0]);
960        assert!((result[1] - 2.0).abs() < 1e-10, "At t=0.5: {}", result[1]);
961        assert!((result[2] - 4.0).abs() < 1e-10, "At t=1: {}", result[2]);
962    }
963
964    #[test]
965    fn test_to_regular_grid_multiple_curves() {
966        let offsets = vec![0, 5, 10];
967        let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.25, 0.5, 0.75, 1.0];
968        let values = vec![
969            0.0, 1.0, 2.0, 3.0, 4.0, // Linear: y = 4t
970            4.0, 3.0, 2.0, 1.0, 0.0, // Linear: y = 4 - 4t
971        ];
972
973        let grid = vec![0.0, 0.5, 1.0];
974        let result = to_regular_grid(&offsets, &argvals, &values, &grid);
975
976        // Should produce 2 curves x 3 points = 6 values in column-major
977        assert_eq!(result.len(), 6);
978
979        // Curve 0 at t=0.5 should be 2.0
980        assert!((result[2] - 2.0).abs() < 1e-10);
981        // Curve 1 at t=0.5 should be 2.0
982        assert!((result[1 + 2] - 2.0).abs() < 1e-10);
983    }
984
985    #[test]
986    fn test_to_regular_grid_boundary_nan() {
987        let offsets = vec![0, 3];
988        let argvals = vec![0.2, 0.5, 0.8]; // Curve only defined on [0.2, 0.8]
989        let values = vec![1.0, 2.0, 3.0];
990
991        let grid = vec![0.0, 0.5, 1.0]; // Grid extends beyond curve range
992        let result = to_regular_grid(&offsets, &argvals, &values, &grid);
993
994        // At t=0.0 (before curve starts), should be NaN
995        assert!(result[0].is_nan(), "t=0 should be NaN");
996        // At t=0.5 (within range), should be valid
997        assert!((result[1] - 2.0).abs() < 1e-10, "t=0.5: {}", result[1]);
998        // At t=1.0 (after curve ends), should be NaN
999        assert!(result[2].is_nan(), "t=1 should be NaN");
1000    }
1001}