Skip to main content

dspk_synapse/io/probe/
sensor.rs

1use serde::{Deserialize, Serialize};
2use super::site::{Position3D, SensorSite};
3use dspk_core::{DspError, DspResult};
4
5/// Physical sensor array geometry layout (e.g. multi-electrode arrays, acoustic arrays).
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct SensorLayout {
8    pub name: String,
9    pub contacts: Vec<SensorSite>,
10}
11
12impl SensorLayout {
13    pub fn new(name: impl Into<String>, contacts: Vec<SensorSite>) -> Self {
14        Self {
15            name: name.into(),
16            contacts,
17        }
18    }
19
20    pub fn total_channels(&self) -> usize {
21        self.contacts.len()
22    }
23
24    pub fn active_channels(&self) -> usize {
25        self.contacts.iter().filter(|c| c.enabled).count()
26    }
27
28    /// Site of recording channel `channel_id`. O(1) when sites are stored in channel order (the
29    /// usual case), a scan otherwise.
30    pub fn get_site(&self, channel_id: usize) -> DspResult<&SensorSite> {
31        if let Some(site) = self.contacts.get(channel_id).filter(|c| c.channel_id == channel_id) {
32            return Ok(site);
33        }
34        self.contacts
35            .iter()
36            .find(|c| c.channel_id == channel_id)
37            .ok_or(DspError::InvalidChannel {
38                channel: channel_id,
39                total: self.contacts.len(),
40            })
41    }
42
43    pub fn get_contact(&self, channel_id: usize) -> DspResult<&SensorSite> {
44        self.get_site(channel_id)
45    }
46
47    pub fn sites(&self) -> &[SensorSite] {
48        &self.contacts
49    }
50
51    /// Layout restricted to `channels` (recording channel ids of the parent), renumbered so that
52    /// the site of `channels[i]` gets channel id `i` — the layout of a channel subset.
53    pub fn select_channels(&self, channels: &[usize]) -> Self {
54        let contacts = channels
55            .iter()
56            .enumerate()
57            .filter_map(|(i, &c)| self.get_site(c).ok().map(|s| SensorSite { channel_id: i, ..s.clone() }))
58            .collect();
59        Self { name: self.name.clone(), contacts }
60    }
61
62    /// Flat `(channel_map, channel_positions_xy, channel_shanks)` arrays as used by Phy / Kilosort
63    /// and Zarr stores.
64    pub fn to_channel_arrays(&self) -> (Vec<usize>, Vec<[f32; 2]>, Vec<usize>) {
65        let map = self.contacts.iter().map(|c| c.channel_id).collect();
66        let pos = self.contacts.iter().map(|c| [c.position.x_um, c.position.y_um]).collect();
67        let shanks = self.contacts.iter().map(|c| c.shank_id).collect();
68        (map, pos, shanks)
69    }
70
71    /// Reconstructs a `SensorLayout` from flat `(channel_map, channel_positions_xy, channel_shanks)`
72    /// arrays (`None` when `positions` is empty). Missing `channel_map` or `shanks` entries fall back
73    /// to identity (`i`) and shank `0`.
74    pub fn from_channel_arrays(
75        name: impl Into<String>,
76        channel_map: &[usize],
77        positions: &[[f32; 2]],
78        shanks: &[usize],
79    ) -> Option<Self> {
80        if positions.is_empty() {
81            return None;
82        }
83        let contacts = positions
84            .iter()
85            .enumerate()
86            .map(|(i, p)| {
87                SensorSite::new(
88                    channel_map.get(i).copied().unwrap_or(i),
89                    Position3D::new(p[0], p[1], 0.0),
90                    shanks.get(i).copied().unwrap_or(0),
91                )
92            })
93            .collect();
94        Some(Self::new(name, contacts))
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    #[test]
101    fn neuropixels_1_0_matches_probeinterface() {
102        let np = crate::io::probe::neuropixels_1_0();
103        let xy: Vec<(f32, f32)> = np.contacts[..6].iter().map(|c| (c.position.x_um, c.position.y_um)).collect();
104        assert_eq!(xy, vec![(43.0, 0.0), (11.0, 0.0), (59.0, 20.0), (27.0, 20.0), (43.0, 40.0), (11.0, 40.0)]);
105        assert_eq!(np.get_site(383).unwrap().position.y_um, 3820.0);
106    }
107
108    #[test]
109    fn select_channels_renumbers_sites() {
110        let np = crate::io::probe::neuropixels_1_0();
111        let sub = np.select_channels(&[10, 3, 200]);
112        assert_eq!(sub.contacts.len(), 3);
113        assert_eq!(sub.get_site(1).unwrap().position, np.get_site(3).unwrap().position);
114        assert_eq!(sub.get_site(2).unwrap().position, np.get_site(200).unwrap().position);
115    }
116}