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