use crate::{matrix::FdMatrix, FdarError};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FdComponent {
pub data: FdMatrix,
pub argvals: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MultiFunData {
components: Vec<FdComponent>,
}
impl MultiFunData {
pub fn new(components: Vec<FdComponent>) -> Result<Self, FdarError> {
if components.is_empty() {
return Err(FdarError::InvalidParameter {
parameter: "components",
message: "MultiFunData requires at least one component".to_string(),
});
}
let n_obs = components[0].data.nrows();
if components[0].argvals.len() != components[0].data.ncols() {
return Err(FdarError::InvalidDimension {
parameter: "components[0].argvals",
expected: format!("{}", components[0].data.ncols()),
actual: format!("{}", components[0].argvals.len()),
});
}
for (k, comp) in components.iter().enumerate().skip(1) {
if comp.data.nrows() != n_obs {
return Err(FdarError::InvalidDimension {
parameter: "components[k].data.nrows",
expected: format!("{n_obs} (same as component 0)"),
actual: format!("{} (component {k})", comp.data.nrows()),
});
}
if comp.argvals.len() != comp.data.ncols() {
return Err(FdarError::InvalidDimension {
parameter: "components[k].argvals",
expected: format!("{} (data.ncols for component {k})", comp.data.ncols()),
actual: format!("{}", comp.argvals.len()),
});
}
}
Ok(Self { components })
}
#[inline]
pub fn n_obs(&self) -> usize {
self.components[0].data.nrows()
}
#[inline]
pub fn n_components(&self) -> usize {
self.components.len()
}
pub fn component(&self, k: usize) -> Result<&FdComponent, FdarError> {
if k >= self.components.len() {
return Err(FdarError::InvalidParameter {
parameter: "k",
message: format!(
"component index {k} out of range (n_components = {})",
self.components.len()
),
});
}
Ok(&self.components[k])
}
pub fn argvals(&self, k: usize) -> Result<&[f64], FdarError> {
if k >= self.components.len() {
return Err(FdarError::InvalidParameter {
parameter: "k",
message: format!(
"argvals index {k} out of range (n_components = {})",
self.components.len()
),
});
}
Ok(&self.components[k].argvals)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::matrix::FdMatrix;
fn make_component(nrows: usize, ncols: usize) -> FdComponent {
FdComponent {
data: FdMatrix::zeros(nrows, ncols),
argvals: (0..ncols).map(|i| i as f64).collect(),
}
}
fn make_component_argvals(nrows: usize, argvals: Vec<f64>) -> FdComponent {
let ncols = argvals.len();
FdComponent {
data: FdMatrix::zeros(nrows, ncols),
argvals,
}
}
#[test]
fn test_two_component_different_grids_ok() {
let comp1 = make_component(5, 10);
let comp2 = make_component(5, 4);
let mfd = MultiFunData::new(vec![comp1, comp2]).unwrap();
assert_eq!(mfd.n_obs(), 5);
assert_eq!(mfd.n_components(), 2);
}
#[test]
fn test_single_component_ok() {
let comp = make_component(3, 6);
let mfd = MultiFunData::new(vec![comp]).unwrap();
assert_eq!(mfd.n_obs(), 3);
assert_eq!(mfd.n_components(), 1);
}
#[test]
fn test_three_components_same_nrows_ok() {
let comp1 = make_component(7, 5);
let comp2 = make_component(7, 10);
let comp3 = make_component(7, 3);
let mfd = MultiFunData::new(vec![comp1, comp2, comp3]).unwrap();
assert_eq!(mfd.n_obs(), 7);
assert_eq!(mfd.n_components(), 3);
}
#[test]
fn test_empty_components_err() {
let result = MultiFunData::new(vec![]);
assert!(matches!(result, Err(FdarError::InvalidParameter { .. })));
}
#[test]
fn test_mismatched_nrows_err() {
let comp1 = make_component(5, 10);
let comp2 = make_component(4, 10); let result = MultiFunData::new(vec![comp1, comp2]);
assert!(matches!(result, Err(FdarError::InvalidDimension { .. })));
}
#[test]
fn test_argvals_len_mismatch_first_component_err() {
let comp = FdComponent {
data: FdMatrix::zeros(5, 10),
argvals: vec![0.0, 1.0, 2.0], };
let result = MultiFunData::new(vec![comp]);
assert!(matches!(result, Err(FdarError::InvalidDimension { .. })));
}
#[test]
fn test_argvals_len_mismatch_later_component_err() {
let comp1 = make_component(5, 10);
let comp2 = FdComponent {
data: FdMatrix::zeros(5, 4),
argvals: vec![0.0, 1.0], };
let result = MultiFunData::new(vec![comp1, comp2]);
assert!(matches!(result, Err(FdarError::InvalidDimension { .. })));
}
#[test]
fn test_component_accessor_valid() {
let argvals1: Vec<f64> = (0..10).map(|i| i as f64 / 9.0).collect();
let argvals2: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0];
let comp1 = make_component_argvals(5, argvals1.clone());
let comp2 = make_component_argvals(5, argvals2.clone());
let mfd = MultiFunData::new(vec![comp1, comp2]).unwrap();
let c0 = mfd.component(0).unwrap();
assert_eq!(c0.argvals, argvals1);
assert_eq!(c0.data.nrows(), 5);
assert_eq!(c0.data.ncols(), 10);
let c1 = mfd.component(1).unwrap();
assert_eq!(c1.argvals, argvals2);
assert_eq!(c1.data.ncols(), 4);
}
#[test]
fn test_component_accessor_out_of_range_err() {
let mfd = MultiFunData::new(vec![make_component(3, 5)]).unwrap();
let result = mfd.component(1);
assert!(matches!(result, Err(FdarError::InvalidParameter { .. })));
}
#[test]
fn test_argvals_accessor_valid() {
let argvals: Vec<f64> = vec![0.0, 0.5, 1.0];
let comp = make_component_argvals(4, argvals.clone());
let mfd = MultiFunData::new(vec![comp]).unwrap();
assert_eq!(mfd.argvals(0).unwrap(), argvals.as_slice());
}
#[test]
fn test_argvals_accessor_out_of_range_err() {
let mfd = MultiFunData::new(vec![make_component(3, 5)]).unwrap();
let result = mfd.argvals(5);
assert!(matches!(result, Err(FdarError::InvalidParameter { .. })));
}
#[test]
fn test_component_accessor_preserves_argvals_per_component() {
let argvals1: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let argvals2: Vec<f64> = vec![10.0, 20.0];
let comp1 = make_component_argvals(6, argvals1.clone());
let comp2 = make_component_argvals(6, argvals2.clone());
let mfd = MultiFunData::new(vec![comp1, comp2]).unwrap();
assert_eq!(mfd.argvals(0).unwrap(), argvals1.as_slice());
assert_eq!(mfd.argvals(1).unwrap(), argvals2.as_slice());
}
#[test]
fn test_no_panic_on_out_of_range_component() {
let mfd = MultiFunData::new(vec![make_component(2, 3)]).unwrap();
assert!(mfd.component(100).is_err());
assert!(mfd.argvals(100).is_err());
assert!(mfd.component(usize::MAX).is_err());
}
#[test]
fn test_debug_clone_partialeq() {
let comp = make_component(2, 3);
let mfd = MultiFunData::new(vec![comp]).unwrap();
let mfd2 = mfd.clone();
assert_eq!(mfd, mfd2);
let s = format!("{:?}", mfd);
assert!(s.contains("MultiFunData"));
}
#[test]
fn test_fdcomponent_debug_clone_partialeq() {
let comp = make_component(2, 4);
let comp2 = comp.clone();
assert_eq!(comp, comp2);
let s = format!("{:?}", comp);
assert!(s.contains("FdComponent"));
}
}