Skip to main content

fdars_core/irreg_fdata/
mod.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
23pub mod kernels;
24pub mod smoothing;
25
26#[cfg(test)]
27mod tests;
28
29// Re-export all public items
30pub use kernels::{mean_irreg, KernelType};
31pub use smoothing::{cov_irreg, integrate_irreg, metric_lp_irreg, norm_lp_irreg, to_regular_grid};
32
33/// Compressed storage for irregular functional data.
34///
35/// Uses CSR-style layout where each observation can have a different
36/// number of evaluation points.
37#[derive(Clone, Debug)]
38pub struct IrregFdata {
39    /// Start indices for each observation (length n+1)
40    /// `offsets[i]..offsets[i+1]` gives the range for observation i
41    pub offsets: Vec<usize>,
42    /// All observation points concatenated
43    pub argvals: Vec<f64>,
44    /// All values concatenated
45    pub values: Vec<f64>,
46    /// Domain range `[min, max]`
47    pub rangeval: [f64; 2],
48}
49
50impl IrregFdata {
51    /// Create from lists of argvals and values (one per observation).
52    ///
53    /// # Arguments
54    /// * `argvals_list` - List of observation point vectors
55    /// * `values_list` - List of value vectors (same lengths as argvals_list)
56    ///
57    /// # Panics
58    /// Panics if the lists have different lengths or if any pair has mismatched lengths.
59    pub fn from_lists(argvals_list: &[Vec<f64>], values_list: &[Vec<f64>]) -> Self {
60        let n = argvals_list.len();
61        assert_eq!(
62            n,
63            values_list.len(),
64            "argvals_list and values_list must have same length"
65        );
66
67        let mut offsets = Vec::with_capacity(n + 1);
68        offsets.push(0);
69
70        let total_points: usize = argvals_list.iter().map(std::vec::Vec::len).sum();
71        let mut argvals = Vec::with_capacity(total_points);
72        let mut values = Vec::with_capacity(total_points);
73
74        let mut range_min = f64::INFINITY;
75        let mut range_max = f64::NEG_INFINITY;
76
77        for i in 0..n {
78            assert_eq!(
79                argvals_list[i].len(),
80                values_list[i].len(),
81                "Observation {i} has mismatched argvals/values lengths"
82            );
83
84            argvals.extend_from_slice(&argvals_list[i]);
85            values.extend_from_slice(&values_list[i]);
86            offsets.push(argvals.len());
87
88            if let (Some(&min), Some(&max)) = (argvals_list[i].first(), argvals_list[i].last()) {
89                range_min = range_min.min(min);
90                range_max = range_max.max(max);
91            }
92        }
93
94        IrregFdata {
95            offsets,
96            argvals,
97            values,
98            rangeval: [range_min, range_max],
99        }
100    }
101
102    /// Create from flattened representation (for R interop).
103    ///
104    /// Returns `None` if offsets are empty, argvals/values lengths differ,
105    /// the last offset doesn't match argvals length, or offsets are non-monotonic.
106    ///
107    /// # Arguments
108    /// * `offsets` - Start indices (length n+1)
109    /// * `argvals` - All observation points concatenated
110    /// * `values` - All values concatenated
111    /// * `rangeval` - Domain range `[min, max]`
112    pub fn from_flat(
113        offsets: Vec<usize>,
114        argvals: Vec<f64>,
115        values: Vec<f64>,
116        rangeval: [f64; 2],
117    ) -> Result<Self, crate::FdarError> {
118        let last = offsets.last().copied().unwrap_or(0);
119        if offsets.is_empty()
120            || argvals.len() != values.len()
121            || last != argvals.len()
122            || offsets.windows(2).any(|w| w[0] > w[1])
123        {
124            return Err(crate::FdarError::InvalidDimension {
125                parameter: "offsets/argvals/values",
126                expected: "non-empty offsets, argvals.len() == values.len(), monotone offsets"
127                    .to_string(),
128                actual: format!(
129                    "offsets.len()={}, argvals.len()={}, values.len()={}",
130                    offsets.len(),
131                    argvals.len(),
132                    values.len()
133                ),
134            });
135        }
136        Ok(IrregFdata {
137            offsets,
138            argvals,
139            values,
140            rangeval,
141        })
142    }
143
144    /// Number of observations stored in this irregular functional data object.
145    #[inline]
146    pub fn n_obs(&self) -> usize {
147        self.offsets.len().saturating_sub(1)
148    }
149
150    /// Number of points for observation i.
151    #[inline]
152    pub fn n_points(&self, i: usize) -> usize {
153        self.offsets[i + 1] - self.offsets[i]
154    }
155
156    /// Get observation i as a pair of slices (argvals, values).
157    #[inline]
158    pub fn get_obs(&self, i: usize) -> (&[f64], &[f64]) {
159        let start = self.offsets[i];
160        let end = self.offsets[i + 1];
161        (&self.argvals[start..end], &self.values[start..end])
162    }
163
164    /// Total number of observation points across all curves.
165    #[inline]
166    pub fn total_points(&self) -> usize {
167        self.argvals.len()
168    }
169
170    /// Get observation counts for all curves.
171    pub fn obs_counts(&self) -> Vec<usize> {
172        (0..self.n_obs()).map(|i| self.n_points(i)).collect()
173    }
174
175    /// Get minimum number of observations per curve.
176    pub fn min_obs(&self) -> usize {
177        (0..self.n_obs())
178            .map(|i| self.n_points(i))
179            .min()
180            .unwrap_or(0)
181    }
182
183    /// Get maximum number of observations per curve.
184    pub fn max_obs(&self) -> usize {
185        (0..self.n_obs())
186            .map(|i| self.n_points(i))
187            .max()
188            .unwrap_or(0)
189    }
190}
191
192/// Linear interpolation at point t.
193pub(super) fn linear_interp(argvals: &[f64], values: &[f64], t: f64) -> f64 {
194    if t <= argvals[0] {
195        return values[0];
196    }
197    if t >= argvals[argvals.len() - 1] {
198        return values[values.len() - 1];
199    }
200
201    // Find the interval
202    let idx = argvals
203        .iter()
204        .position(|&x| x > t)
205        .expect("element must exist in collection");
206    let t0 = argvals[idx - 1];
207    let t1 = argvals[idx];
208    let x0 = values[idx - 1];
209    let x1 = values[idx];
210
211    // Linear interpolation
212    x0 + (x1 - x0) * (t - t0) / (t1 - t0)
213}