Skip to main content

fdars_core/
validation.rs

1//! Common input validation helpers.
2//!
3//! These utilities centralise the dimension-checking boilerplate that recurs
4//! across regression, classification, depth, and alignment entry points.
5
6use crate::error::FdarError;
7use crate::matrix::FdMatrix;
8
9/// Validate functional data dimensions.
10///
11/// Checks that `data` has at least 1 row and 1 column, and that `argvals`
12/// length matches the number of columns.
13///
14/// Returns `(n, m)` on success.
15///
16/// # Errors
17///
18/// Returns [`FdarError::InvalidDimension`] when any check fails.
19///
20/// # Examples
21///
22/// ```
23/// use fdars_core::validation::validate_fdata;
24/// use fdars_core::matrix::FdMatrix;
25///
26/// let data = FdMatrix::zeros(10, 50);
27/// let t: Vec<f64> = (0..50).map(|i| i as f64).collect();
28/// let (n, m) = validate_fdata(&data, &t).unwrap();
29/// assert_eq!((n, m), (10, 50));
30/// ```
31pub fn validate_fdata(data: &FdMatrix, argvals: &[f64]) -> Result<(usize, usize), FdarError> {
32    let (n, m) = data.shape();
33    if n == 0 {
34        return Err(FdarError::InvalidDimension {
35            parameter: "data",
36            expected: "n > 0 rows".to_string(),
37            actual: format!("n = {n}"),
38        });
39    }
40    if m == 0 {
41        return Err(FdarError::InvalidDimension {
42            parameter: "data",
43            expected: "m > 0 columns".to_string(),
44            actual: format!("m = {m}"),
45        });
46    }
47    if argvals.len() != m {
48        return Err(FdarError::InvalidDimension {
49            parameter: "argvals",
50            expected: format!("{m} elements"),
51            actual: format!("{} elements", argvals.len()),
52        });
53    }
54    Ok((n, m))
55}
56
57/// Validate that a response vector matches the data row count.
58///
59/// # Errors
60///
61/// Returns [`FdarError::InvalidDimension`] if `y.len() != n`.
62///
63/// # Examples
64///
65/// ```
66/// use fdars_core::validation::validate_response;
67///
68/// validate_response(&[1.0, 2.0, 3.0], 3).unwrap();
69/// assert!(validate_response(&[1.0, 2.0], 3).is_err());
70/// ```
71pub fn validate_response(y: &[f64], n: usize) -> Result<(), FdarError> {
72    if y.len() != n {
73        return Err(FdarError::InvalidDimension {
74            parameter: "y",
75            expected: format!("{n} elements"),
76            actual: format!("{} elements", y.len()),
77        });
78    }
79    Ok(())
80}
81
82/// Validate class labels match data dimensions and have at least `min_classes` classes.
83///
84/// Labels are expected to be 0-indexed. Returns the number of distinct
85/// classes (i.e. `max(y) + 1`).
86///
87/// # Errors
88///
89/// Returns [`FdarError::InvalidDimension`] if `y.len() != n`, or
90/// [`FdarError::InvalidParameter`] if fewer than `min_classes` classes are found.
91///
92/// # Examples
93///
94/// ```
95/// use fdars_core::validation::validate_labels;
96///
97/// let n_classes = validate_labels(&[0, 1, 0, 1], 4, 2).unwrap();
98/// assert_eq!(n_classes, 2);
99/// assert!(validate_labels(&[0, 0, 0], 3, 2).is_err());
100/// ```
101pub fn validate_labels(y: &[usize], n: usize, min_classes: usize) -> Result<usize, FdarError> {
102    if y.len() != n {
103        return Err(FdarError::InvalidDimension {
104            parameter: "y",
105            expected: format!("{n} elements"),
106            actual: format!("{} elements", y.len()),
107        });
108    }
109    let n_classes = y.iter().copied().max().map_or(0, |m| m + 1);
110    if n_classes < min_classes {
111        return Err(FdarError::InvalidParameter {
112            parameter: "y",
113            message: format!("need at least {min_classes} classes, got {n_classes}"),
114        });
115    }
116    Ok(n_classes)
117}
118
119/// Validate that a distance matrix is square and optionally matches an expected size.
120///
121/// Returns the number of rows/columns `n`.
122///
123/// # Errors
124///
125/// Returns [`FdarError::InvalidDimension`] if the matrix is not square or
126/// does not match the expected size.
127///
128/// # Examples
129///
130/// ```
131/// use fdars_core::validation::validate_dist_mat;
132/// use fdars_core::matrix::FdMatrix;
133///
134/// let dm = FdMatrix::zeros(5, 5);
135/// assert_eq!(validate_dist_mat(&dm, None).unwrap(), 5);
136/// assert_eq!(validate_dist_mat(&dm, Some(5)).unwrap(), 5);
137/// assert!(validate_dist_mat(&dm, Some(4)).is_err());
138/// ```
139pub fn validate_dist_mat(
140    dist_mat: &FdMatrix,
141    expected_n: Option<usize>,
142) -> Result<usize, FdarError> {
143    let n = dist_mat.nrows();
144    if dist_mat.ncols() != n {
145        return Err(FdarError::InvalidDimension {
146            parameter: "dist_mat",
147            expected: format!("{n} x {n} (square)"),
148            actual: format!("{} x {}", n, dist_mat.ncols()),
149        });
150    }
151    if let Some(exp) = expected_n {
152        if n != exp {
153            return Err(FdarError::InvalidDimension {
154                parameter: "dist_mat",
155                expected: format!("{exp} x {exp}"),
156                actual: format!("{n} x {n}"),
157            });
158        }
159    }
160    Ok(n)
161}
162
163/// Validate and clamp the `ncomp` parameter.
164///
165/// Returns `min(ncomp, n, m)` after ensuring `ncomp >= 1`.
166///
167/// # Errors
168///
169/// Returns [`FdarError::InvalidParameter`] if `ncomp == 0`.
170///
171/// # Examples
172///
173/// ```
174/// use fdars_core::validation::validate_ncomp;
175///
176/// assert_eq!(validate_ncomp(5, 10, 20).unwrap(), 5);
177/// assert_eq!(validate_ncomp(100, 10, 20).unwrap(), 10);
178/// assert!(validate_ncomp(0, 10, 20).is_err());
179/// ```
180pub fn validate_ncomp(ncomp: usize, n: usize, m: usize) -> Result<usize, FdarError> {
181    if ncomp == 0 {
182        return Err(FdarError::InvalidParameter {
183            parameter: "ncomp",
184            message: "must be >= 1".to_string(),
185        });
186    }
187    Ok(ncomp.min(n).min(m))
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    // ── validate_fdata ──────────────────────────────────────────────────
195
196    #[test]
197    fn fdata_ok() {
198        let data = FdMatrix::zeros(10, 50);
199        let t: Vec<f64> = (0..50).map(|i| i as f64).collect();
200        let (n, m) = validate_fdata(&data, &t).unwrap();
201        assert_eq!((n, m), (10, 50));
202    }
203
204    #[test]
205    fn fdata_zero_rows() {
206        let data = FdMatrix::zeros(0, 5);
207        let t = vec![0.0; 5];
208        assert!(validate_fdata(&data, &t).is_err());
209    }
210
211    #[test]
212    fn fdata_zero_cols() {
213        let data = FdMatrix::zeros(5, 0);
214        assert!(validate_fdata(&data, &[]).is_err());
215    }
216
217    #[test]
218    fn fdata_argvals_mismatch() {
219        let data = FdMatrix::zeros(5, 10);
220        let t = vec![0.0; 8];
221        assert!(validate_fdata(&data, &t).is_err());
222    }
223
224    // ── validate_response ───────────────────────────────────────────────
225
226    #[test]
227    fn response_ok() {
228        validate_response(&[1.0, 2.0, 3.0], 3).unwrap();
229    }
230
231    #[test]
232    fn response_mismatch() {
233        assert!(validate_response(&[1.0, 2.0], 3).is_err());
234    }
235
236    // ── validate_labels ─────────────────────────────────────────────────
237
238    #[test]
239    fn labels_ok() {
240        let nc = validate_labels(&[0, 1, 0, 1], 4, 2).unwrap();
241        assert_eq!(nc, 2);
242    }
243
244    #[test]
245    fn labels_too_few_classes() {
246        assert!(validate_labels(&[0, 0, 0], 3, 2).is_err());
247    }
248
249    #[test]
250    fn labels_length_mismatch() {
251        assert!(validate_labels(&[0, 1], 3, 2).is_err());
252    }
253
254    // ── validate_dist_mat ───────────────────────────────────────────────
255
256    #[test]
257    fn dist_mat_ok() {
258        let dm = FdMatrix::zeros(5, 5);
259        assert_eq!(validate_dist_mat(&dm, None).unwrap(), 5);
260        assert_eq!(validate_dist_mat(&dm, Some(5)).unwrap(), 5);
261    }
262
263    #[test]
264    fn dist_mat_not_square() {
265        let dm = FdMatrix::zeros(5, 3);
266        assert!(validate_dist_mat(&dm, None).is_err());
267    }
268
269    #[test]
270    fn dist_mat_wrong_size() {
271        let dm = FdMatrix::zeros(5, 5);
272        assert!(validate_dist_mat(&dm, Some(4)).is_err());
273    }
274
275    // ── validate_ncomp ──────────────────────────────────────────────────
276
277    #[test]
278    fn ncomp_ok() {
279        assert_eq!(validate_ncomp(5, 10, 20).unwrap(), 5);
280    }
281
282    #[test]
283    fn ncomp_clamped() {
284        assert_eq!(validate_ncomp(100, 10, 20).unwrap(), 10);
285        assert_eq!(validate_ncomp(100, 20, 10).unwrap(), 10);
286    }
287
288    #[test]
289    fn ncomp_zero() {
290        assert!(validate_ncomp(0, 10, 20).is_err());
291    }
292}