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