modewise_pipeline/
artifact.rs

1use std::fmt;
2
3use modewise_brain::ChannelTable;
4use modewise_connectivity::FunctionalConnectivity;
5use modewise_decomposition::{Decomposition, InstantaneousModes, Loadings};
6use modewise_epochs::{Epochs, Events};
7use modewise_signals::MultichannelSeries;
8use modewise_spectral::TimeFrequencyMap;
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12/// Data that flows between pipeline steps.
13#[derive(Debug, Clone, PartialEq)]
14pub enum Artifact {
15    Series(MultichannelSeries),
16    Decomposition(Decomposition),
17    TimeFrequency(TimeFrequencyMap),
18    Events(Events),
19    Epochs(Epochs),
20    Instantaneous(InstantaneousModes),
21    Loadings(Loadings),
22    FunctionalConnectivity(FunctionalConnectivity),
23    Channels(ChannelTable),
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum ArtifactKind {
29    Series,
30    Decomposition,
31    TimeFrequency,
32    Events,
33    Epochs,
34    Instantaneous,
35    Loadings,
36    FunctionalConnectivity,
37    Channels,
38}
39
40impl Artifact {
41    pub fn kind(&self) -> ArtifactKind {
42        match self {
43            Self::Series(_) => ArtifactKind::Series,
44            Self::Decomposition(_) => ArtifactKind::Decomposition,
45            Self::TimeFrequency(_) => ArtifactKind::TimeFrequency,
46            Self::Events(_) => ArtifactKind::Events,
47            Self::Epochs(_) => ArtifactKind::Epochs,
48            Self::Instantaneous(_) => ArtifactKind::Instantaneous,
49            Self::Loadings(_) => ArtifactKind::Loadings,
50            Self::FunctionalConnectivity(_) => ArtifactKind::FunctionalConnectivity,
51            Self::Channels(_) => ArtifactKind::Channels,
52        }
53    }
54}
55
56impl fmt::Display for ArtifactKind {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        let name = match self {
59            Self::Series => "series",
60            Self::Decomposition => "decomposition",
61            Self::TimeFrequency => "time_frequency",
62            Self::Events => "events",
63            Self::Epochs => "epochs",
64            Self::Instantaneous => "instantaneous",
65            Self::Loadings => "loadings",
66            Self::FunctionalConnectivity => "functional_connectivity",
67            Self::Channels => "channels",
68        };
69        f.write_str(name)
70    }
71}
72
73/// SHA-256 content address of an artifact, as lowercase hex.
74#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
75#[serde(transparent)]
76pub struct ArtifactKey(String);
77
78impl ArtifactKey {
79    pub(crate) fn from_bytes(bytes: &[u8]) -> Self {
80        let digest = Sha256::digest(bytes);
81        Self(digest.iter().map(|b| format!("{b:02x}")).collect())
82    }
83
84    /// Keys a pipeline input by its content, so changed data never hits a stale cache.
85    pub(crate) fn of_series(series: &MultichannelSeries) -> Self {
86        let mut bytes = b"series".to_vec();
87        bytes.extend(series.sampling_rate().hz().to_le_bytes());
88        bytes.extend(series.n_channels().to_le_bytes());
89        bytes.extend(series.n_samples().to_le_bytes());
90        for label in series.labels() {
91            bytes.extend(label.len().to_le_bytes());
92            bytes.extend(label.as_bytes());
93        }
94        for value in series.data().iter() {
95            bytes.extend(value.to_le_bytes());
96        }
97        Self::from_bytes(&bytes)
98    }
99
100    pub(crate) fn of_events(events: &Events) -> Self {
101        let mut bytes = b"events".to_vec();
102        for e in events.as_slice() {
103            bytes.extend(e.onset_s.to_le_bytes());
104            bytes.extend(e.duration_s.to_le_bytes());
105            bytes.extend(e.label.len().to_le_bytes());
106            bytes.extend(e.label.as_bytes());
107        }
108        Self::from_bytes(&bytes)
109    }
110
111    pub(crate) fn of_channels(table: &ChannelTable) -> Self {
112        let mut bytes = b"channels".to_vec();
113        let mut text = |s: &str| {
114            bytes.extend(s.len().to_le_bytes());
115            bytes.extend(s.as_bytes());
116        };
117        for c in table.channels() {
118            text(c.name());
119            text(c.region().map_or("\0", |r| r.as_str()));
120            text(c.hemisphere().map_or("\0", |h| h.as_str()));
121            for (k, v) in c.tags() {
122                text(k);
123                text(v);
124            }
125            text("\0end");
126        }
127        Self::from_bytes(&bytes)
128    }
129
130    pub fn as_str(&self) -> &str {
131        &self.0
132    }
133}
134
135impl fmt::Display for ArtifactKey {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(&self.0)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn keys_are_lowercase_sha256_hex() {
147        let key = ArtifactKey::from_bytes(b"abc");
148        assert_eq!(
149            key.as_str(),
150            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
151        );
152        assert_eq!(key.to_string(), key.as_str());
153    }
154}