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