modewise_decomposition/
loadings.rs1use modewise_method::MethodError;
2use ndarray::{Array2, ArrayView2};
3
4#[derive(Debug, Clone, PartialEq)]
7pub struct Loadings {
8 values: Array2<f64>,
9 latent_labels: Vec<String>,
10 channel_labels: Vec<String>,
11}
12
13impl Loadings {
14 pub fn new(
15 values: Array2<f64>,
16 latent_labels: Vec<String>,
17 channel_labels: Vec<String>,
18 ) -> Result<Self, MethodError> {
19 let (n_latents, n_channels) = values.dim();
20 if latent_labels.len() != n_latents || channel_labels.len() != n_channels {
21 return Err(MethodError::ShapeMismatch(format!(
22 "loadings are {n_latents} × {n_channels} but there are {} latent and {} channel labels",
23 latent_labels.len(),
24 channel_labels.len()
25 )));
26 }
27 Ok(Self {
28 values,
29 latent_labels,
30 channel_labels,
31 })
32 }
33
34 pub fn values(&self) -> ArrayView2<'_, f64> {
35 self.values.view()
36 }
37
38 pub fn latent_labels(&self) -> &[String] {
39 &self.latent_labels
40 }
41
42 pub fn channel_labels(&self) -> &[String] {
43 &self.channel_labels
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 #[test]
52 fn label_counts_must_match_the_matrix() {
53 let labels = |n: usize| (0..n).map(|i| i.to_string()).collect::<Vec<_>>();
54 let ok = Loadings::new(Array2::ones((2, 3)), labels(2), labels(3)).unwrap();
55 assert_eq!(
56 (ok.latent_labels(), ok.channel_labels()),
57 (&labels(2)[..], &labels(3)[..])
58 );
59 assert_eq!(ok.values(), Array2::<f64>::ones((2, 3)));
60 assert!(Loadings::new(Array2::zeros((2, 3)), labels(3), labels(3)).is_err());
61 assert!(Loadings::new(Array2::zeros((2, 3)), labels(2), labels(2)).is_err());
62 }
63}