use serde::{Deserialize, Serialize};
use super::site::{Position3D, SensorSite};
use dspk_core::{DspError, DspResult};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorLayout {
pub name: String,
pub contacts: Vec<SensorSite>,
}
impl SensorLayout {
pub fn new(name: impl Into<String>, contacts: Vec<SensorSite>) -> Self {
Self {
name: name.into(),
contacts,
}
}
pub fn total_channels(&self) -> usize {
self.contacts.len()
}
pub fn active_channels(&self) -> usize {
self.contacts.iter().filter(|c| c.enabled).count()
}
pub fn get_site(&self, channel_id: usize) -> DspResult<&SensorSite> {
if let Some(site) = self.contacts.get(channel_id).filter(|c| c.channel_id == channel_id) {
return Ok(site);
}
self.contacts
.iter()
.find(|c| c.channel_id == channel_id)
.ok_or(DspError::InvalidChannel {
channel: channel_id,
total: self.contacts.len(),
})
}
pub fn get_contact(&self, channel_id: usize) -> DspResult<&SensorSite> {
self.get_site(channel_id)
}
pub fn sites(&self) -> &[SensorSite] {
&self.contacts
}
pub fn select_channels(&self, channels: &[usize]) -> Self {
let contacts = channels
.iter()
.enumerate()
.filter_map(|(i, &c)| self.get_site(c).ok().map(|s| SensorSite { channel_id: i, ..s.clone() }))
.collect();
Self { name: self.name.clone(), contacts }
}
pub fn to_channel_arrays(&self) -> (Vec<usize>, Vec<[f32; 2]>, Vec<usize>) {
let map = self.contacts.iter().map(|c| c.channel_id).collect();
let pos = self.contacts.iter().map(|c| [c.position.x_um, c.position.y_um]).collect();
let shanks = self.contacts.iter().map(|c| c.shank_id).collect();
(map, pos, shanks)
}
pub fn from_channel_arrays(
name: impl Into<String>,
channel_map: &[usize],
positions: &[[f32; 2]],
shanks: &[usize],
) -> Option<Self> {
if positions.is_empty() {
return None;
}
let contacts = positions
.iter()
.enumerate()
.map(|(i, p)| {
SensorSite::new(
channel_map.get(i).copied().unwrap_or(i),
Position3D::new(p[0], p[1], 0.0),
shanks.get(i).copied().unwrap_or(0),
)
})
.collect();
Some(Self::new(name, contacts))
}
}
#[cfg(test)]
mod tests {
#[test]
fn neuropixels_1_0_matches_probeinterface() {
let np = crate::io::probe::neuropixels_1_0();
let xy: Vec<(f32, f32)> = np.contacts[..6].iter().map(|c| (c.position.x_um, c.position.y_um)).collect();
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)]);
assert_eq!(np.get_site(383).unwrap().position.y_um, 3820.0);
}
#[test]
fn select_channels_renumbers_sites() {
let np = crate::io::probe::neuropixels_1_0();
let sub = np.select_channels(&[10, 3, 200]);
assert_eq!(sub.contacts.len(), 3);
assert_eq!(sub.get_site(1).unwrap().position, np.get_site(3).unwrap().position);
assert_eq!(sub.get_site(2).unwrap().position, np.get_site(200).unwrap().position);
}
}