modewise_decomposition/
loadings.rs

1use modewise_method::MethodError;
2use ndarray::{Array2, ArrayView2};
3
4/// How strongly each latent component contributes to each channel, laid out as latent × channel
5/// (the coefficient matrix `A` of a latent model `X = ZA`).
6#[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}