1use std::collections::BTreeMap;
16use std::path::Path;
17
18use dspk_core::{ChannelMap, ChannelPair, DspError, DspResult};
19use serde::Deserialize;
20use serde_json::Value;
21
22use super::{Position3D, SensorLayout, SensorSite};
23
24#[derive(Debug, Clone, PartialEq)]
27pub struct ProbeWiring {
28 pub map: ChannelMap,
29 pub layout: SensorLayout,
30}
31
32fn bad(path: &str, m: impl std::fmt::Display) -> DspError {
33 DspError::InvalidConfig(format!("{path}: {m}"))
34}
35
36pub fn read_probe_wiring(path: &Path) -> DspResult<ProbeWiring> {
39 if !path.extension().is_some_and(|e| e.eq_ignore_ascii_case("json")) {
40 return Err(bad(&path.display().to_string(), "a probe file is a .json file (probeinterface or Kilosort 4); .mat files are not read (neuro-converter converts them)"));
41 }
42 let text = std::fs::read_to_string(path).map_err(|e| bad(&path.display().to_string(), e))?;
43 let name = path.file_stem().map_or_else(|| "probe".into(), |s| s.to_string_lossy().into_owned());
44 parse_probe_wiring(&text, &name).map_err(|e| match e {
45 DspError::InvalidConfig(m) => bad(&path.display().to_string(), m),
46 other => other,
47 })
48}
49
50pub fn parse_probe_wiring(text: &str, name: &str) -> DspResult<ProbeWiring> {
52 let v: Value = serde_json::from_str(text).map_err(|e| bad(name, e))?;
53 if v.get("specification").and_then(Value::as_str) == Some("probeinterface") {
54 probeinterface(v, name)
55 } else if v.get("chanMap").is_some() {
56 kilosort(v, name)
57 } else {
58 Err(bad(name, "neither a probeinterface file (\"specification\": \"probeinterface\") nor a Kilosort probe (\"chanMap\")"))
59 }
60}
61
62struct Contact {
64 channel: usize,
65 position: Position3D,
66 shank: usize,
67 name: Option<String>,
68}
69
70fn wiring(contacts: Vec<Contact>, name: &str) -> DspResult<ProbeWiring> {
71 if contacts.is_empty() {
72 return Err(bad(name, "no connected contacts"));
73 }
74 let pairs = contacts.iter().enumerate().map(|(to, c)| ChannelPair { from: c.channel, to, name: c.name.clone() }).collect();
75 let map = ChannelMap { base: 0, pairs };
76 map.order().map_err(|e| bad(name, e))?;
77 let sites = contacts.into_iter().enumerate().map(|(i, c)| SensorSite::new(i, c.position, c.shank)).collect();
78 Ok(ProbeWiring { map, layout: SensorLayout::new(name, sites) })
79}
80
81fn probeinterface(v: Value, name: &str) -> DspResult<ProbeWiring> {
82 #[derive(Deserialize)]
83 struct Probe {
84 contact_positions: Vec<Vec<f32>>,
85 #[serde(default)]
86 device_channel_indices: Option<Vec<i64>>,
87 #[serde(default)]
88 shank_ids: Option<Vec<String>>,
89 #[serde(default)]
90 contact_ids: Option<Vec<String>>,
91 }
92 #[derive(Deserialize)]
93 struct File {
94 probes: Vec<Probe>,
95 }
96 let file: File = serde_json::from_value(v).map_err(|e| bad(name, e))?;
97 let mut shanks: BTreeMap<(usize, String), usize> = BTreeMap::new();
99 let mut contacts = Vec::new();
100 for (p, probe) in file.probes.iter().enumerate() {
101 let n = probe.contact_positions.len();
102 let Some(dci) = &probe.device_channel_indices else {
103 return Err(bad(name, format!("probe {p} has no device_channel_indices (which channel each contact is wired to)")));
104 };
105 if dci.len() != n {
106 return Err(bad(name, format!("probe {p}: {} device_channel_indices for {n} contacts", dci.len())));
107 }
108 for (i, pos) in probe.contact_positions.iter().enumerate() {
109 let Ok(channel) = usize::try_from(dci[i]) else { continue };
110 let shank_key = (p, probe.shank_ids.as_ref().and_then(|s| s.get(i)).cloned().unwrap_or_default());
111 let next = shanks.len();
112 let shank = *shanks.entry(shank_key).or_insert(next);
113 let at = |k: usize| pos.get(k).copied().unwrap_or(0.0);
114 let name = probe.contact_ids.as_ref().and_then(|c| c.get(i)).filter(|c| !c.is_empty()).cloned();
115 contacts.push(Contact { channel, position: Position3D::new(at(0), at(1), at(2)), shank, name });
116 }
117 }
118 wiring(contacts, name)
119}
120
121fn kilosort(v: Value, name: &str) -> DspResult<ProbeWiring> {
122 #[derive(Deserialize)]
123 #[serde(rename_all = "camelCase")]
124 struct Probe {
125 chan_map: Vec<f64>,
126 #[serde(rename = "xc")]
127 xc: Vec<f32>,
128 #[serde(rename = "yc")]
129 yc: Vec<f32>,
130 #[serde(default)]
131 kcoords: Option<Vec<f64>>,
132 #[serde(default)]
133 connected: Option<Vec<Value>>,
134 }
135 let probe: Probe = serde_json::from_value(v).map_err(|e| bad(name, e))?;
136 let n = probe.chan_map.len();
137 if probe.xc.len() != n || probe.yc.len() != n {
138 return Err(bad(name, format!("chanMap has {n} sites but xc / yc have {} / {}", probe.xc.len(), probe.yc.len())));
139 }
140 let connected = |i: usize| match probe.connected.as_ref().and_then(|c| c.get(i)) {
141 Some(Value::Bool(b)) => *b,
142 Some(Value::Number(x)) => x.as_f64() != Some(0.0),
143 _ => true,
144 };
145 let mut contacts = Vec::new();
146 for i in (0..n).filter(|&i| connected(i)) {
147 let c = probe.chan_map[i];
148 if c < 0.0 || c.fract() != 0.0 {
149 return Err(bad(name, format!("chanMap[{i}] = {c} is not a channel index (0-based)")));
150 }
151 let shank = probe.kcoords.as_ref().and_then(|k| k.get(i)).map_or(0, |&k| k.max(0.0) as usize);
152 contacts.push(Contact { channel: c as usize, position: Position3D::new(probe.xc[i], probe.yc[i], 0.0), shank, name: None });
153 }
154 wiring(contacts, name)
155}
156
157impl SensorLayout {
158 pub fn remap(&self, map: &ChannelMap) -> DspResult<SensorLayout> {
161 let order = map.resolve(self.total_channels())?;
162 for &c in &order {
163 self.get_site(c)?;
164 }
165 Ok(self.select_channels(&order))
166 }
167
168 pub fn depth_order(&self) -> ChannelMap {
171 let mut sites: Vec<&SensorSite> = self.contacts.iter().collect();
172 sites.sort_by(|a, b| {
173 (a.shank_id, a.position.y_um, a.position.x_um)
174 .partial_cmp(&(b.shank_id, b.position.y_um, b.position.x_um))
175 .unwrap_or(std::cmp::Ordering::Equal)
176 });
177 ChannelMap::from_order(&sites.iter().map(|s| s.channel_id).collect::<Vec<_>>())
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 const PROBEINTERFACE: &str = r#"{"specification": "probeinterface", "version": "0.2.21",
186 "probes": [{"ndim": 2, "si_units": "um",
187 "contact_positions": [[0, 0], [0, 20], [16, 10], [16, 30]],
188 "device_channel_indices": [3, 1, -1, 0],
189 "shank_ids": ["", "", "", ""],
190 "contact_ids": ["e0", "e1", "e2", "e3"]}]}"#;
191
192 #[test]
193 fn probeinterface_contacts_in_order_unconnected_left_out() {
194 let w = parse_probe_wiring(PROBEINTERFACE, "p").unwrap();
195 assert_eq!(w.map.order().unwrap(), vec![3, 1, 0]);
196 assert_eq!(w.map.names(), vec![Some("e0".into()), Some("e1".into()), Some("e3".into())]);
197 let ys: Vec<f32> = w.layout.contacts.iter().map(|s| s.position.y_um).collect();
198 assert_eq!(ys, vec![0.0, 20.0, 30.0]);
199 assert_eq!(w.layout.get_site(2).unwrap().position.x_um, 16.0);
200 }
201
202 #[test]
203 fn kilosort_probe_json() {
204 let ks = r#"{"chanMap": [2, 0, 1], "xc": [0, 32, 0], "yc": [0, 0, 15], "kcoords": [0, 0, 1], "n_chan": 3}"#;
205 let w = parse_probe_wiring(ks, "ks").unwrap();
206 assert_eq!(w.map.order().unwrap(), vec![2, 0, 1]);
207 assert_eq!(w.layout.get_site(2).unwrap().shank_id, 1);
208 let unwired = r#"{"chanMap": [0, 1], "xc": [0, 0], "yc": [0, 20], "connected": [true, false]}"#;
209 assert_eq!(parse_probe_wiring(unwired, "ks").unwrap().map.order().unwrap(), vec![0]);
210 assert!(parse_probe_wiring(r#"{"chanMap": [0, 0], "xc": [0, 0], "yc": [0, 1]}"#, "ks").is_err(), "a channel twice");
211 assert!(parse_probe_wiring(r#"{"x": 1}"#, "?").is_err());
212 }
213
214 #[test]
215 fn only_json_files_are_read() {
216 let dir = std::env::temp_dir().join(format!("dspk-wiring-{}", std::process::id()));
217 std::fs::create_dir_all(&dir).unwrap();
218 let mat = dir.join("chanMap.mat");
219 std::fs::write(&mat, b"MATLAB 5.0 MAT-file").unwrap();
220 let err = read_probe_wiring(&mat).unwrap_err().to_string();
221 assert!(err.contains(".json"), "{err}");
222 let json = dir.join("probe.json");
223 std::fs::write(&json, PROBEINTERFACE).unwrap();
224 assert_eq!(read_probe_wiring(&json).unwrap().layout.name, "probe");
225 std::fs::remove_dir_all(&dir).unwrap();
226 }
227
228 #[test]
229 fn geometry_follows_a_map_and_depth_order_sorts_by_shank_and_depth() {
230 let np = crate::io::probe::neuropixels_1_0();
231 let map = ChannelMap::from_order(&[10, 3, 200]);
232 assert_eq!(np.remap(&map).unwrap(), np.select_channels(&[10, 3, 200]));
233 assert!(np.remap(&ChannelMap::from_order(&[384])).is_err());
234 let shuffled = np.select_channels(&[7, 2, 5, 0]);
236 let order = shuffled.depth_order().order().unwrap();
237 let ys: Vec<f32> = order.iter().map(|&c| shuffled.get_site(c).unwrap().position.y_um).collect();
238 assert!(ys.windows(2).all(|w| w[0] <= w[1]), "{ys:?}");
239 }
240}