Skip to main content

dspk_synapse/core/
template.rs

1use dspk_ops::math::peak_to_peak;
2use dspk_ops::math::RunningMoments;
3use crate::io::templates::{DenseTemplates, TemplateAxisOrder};
4use serde::{Deserialize, Serialize};
5use super::snippets::WaveformSnippet;
6
7/// Automated or curated unit quality classification (Allen / IBL / Phy standard).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
9pub enum UnitQualityLabel {
10    /// Well-isolated single biological neuron (`"good"` in Phy)
11    #[serde(alias = "Good", alias = "good", alias = "singleunit")]
12    SingleUnit,
13    /// Multi-unit activity or overlapping cluster (`"mua"` in Phy)
14    #[serde(alias = "Mua", alias = "mua", alias = "multiunit")]
15    MultiUnit,
16    /// Non-biological electrical/motion artifact or thermal noise (`"noise"` in Phy)
17    #[serde(alias = "noise")]
18    Noise,
19    /// Uncurated / unsorted cluster (`"unsorted"` in Phy)
20    #[default]
21    #[serde(alias = "unsorted")]
22    Unsorted,
23}
24
25#[allow(non_upper_case_globals)]
26impl UnitQualityLabel {
27    /// Alias matching Phy's `"good"` group.
28    pub const Good: Self = Self::SingleUnit;
29    /// Alias matching Phy's `"mua"` group.
30    pub const Mua: Self = Self::MultiUnit;
31
32    pub fn as_str(self) -> &'static str {
33        match self {
34            Self::SingleUnit => "good",
35            Self::MultiUnit => "mua",
36            Self::Noise => "noise",
37            Self::Unsorted => "unsorted",
38        }
39    }
40
41    pub fn parse(s: &str) -> Self {
42        match s.trim().to_ascii_lowercase().as_str() {
43            "good" | "singleunit" | "sua" => Self::SingleUnit,
44            "mua" | "multiunit" => Self::MultiUnit,
45            "noise" => Self::Noise,
46            _ => Self::Unsorted,
47        }
48    }
49}
50
51/// Mean action potential template, standard deviation (`std`), and standard error of the mean (`se`)
52/// across a cluster of waveforms.
53///
54/// Row `r` of `mean` / `std` / `se` (each `num_samples` long) belongs to recording channel
55/// `channel_ids[r]`.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct WaveformTemplate {
58    /// Recording channel of each row.
59    pub channel_ids: Vec<usize>,
60    pub num_channels: usize,
61    pub num_samples: usize,
62    /// Number of waveforms accumulated into this template (`>= 1`).
63    pub count: usize,
64    /// Sample of the deepest trough (minimum of `mean` over all rows).
65    pub trough_index: usize,
66    pub mean: Vec<f32>,
67    pub std: Vec<f32>,
68    /// Standard Error of the Mean (`SE = SD / sqrt(count)`).
69    pub se: Vec<f32>,
70}
71
72impl WaveformTemplate {
73    /// Builds a template; `mean` and `std` are `[channel_ids.len(), num_samples]`.
74    pub fn new(channel_ids: Vec<usize>, num_samples: usize, mean: Vec<f32>, std: Vec<f32>) -> Self {
75        Self::with_count(channel_ids, num_samples, 1, mean, std)
76    }
77
78    /// Builds a template with explicit waveform `count`, automatically computing `se = std / sqrt(count)`.
79    pub fn with_count(
80        channel_ids: Vec<usize>,
81        num_samples: usize,
82        count: usize,
83        mean: Vec<f32>,
84        std: Vec<f32>,
85    ) -> Self {
86        let num_channels = channel_ids.len();
87        assert_eq!(mean.len(), num_channels * num_samples, "mean shape");
88        assert_eq!(std.len(), num_channels * num_samples, "std shape");
89        let trough_index = mean
90            .iter()
91            .enumerate()
92            .min_by(|a, b| a.1.total_cmp(b.1))
93            .map_or(0, |(i, _)| i % num_samples.max(1));
94        let denom = (count.max(1) as f32).sqrt();
95        let se = std.iter().map(|&s| s / denom).collect();
96        Self {
97            channel_ids,
98            num_channels,
99            num_samples,
100            count: count.max(1),
101            trough_index,
102            mean,
103            std,
104            se,
105        }
106    }
107
108    /// Mean waveform of row `r`.
109    pub fn row(&self, r: usize) -> &[f32] {
110        &self.mean[r * self.num_samples..(r + 1) * self.num_samples]
111    }
112
113    /// Standard deviation waveform of row `r`.
114    pub fn std_row(&self, r: usize) -> &[f32] {
115        &self.std[r * self.num_samples..(r + 1) * self.num_samples]
116    }
117
118    /// Standard error waveform of row `r`.
119    pub fn se_row(&self, r: usize) -> &[f32] {
120        &self.se[r * self.num_samples..(r + 1) * self.num_samples]
121    }
122
123    /// Mean waveform on recording channel `channel`, if the template covers it.
124    pub fn channel_row(&self, channel: usize) -> Option<&[f32]> {
125        self.channel_ids.iter().position(|&c| c == channel).map(|r| self.row(r))
126    }
127
128    /// Recording channel with the largest peak-to-peak swing in `mean`.
129    pub fn best_channel(&self) -> Option<usize> {
130        (0..self.num_channels)
131            .max_by(|&a, &b| peak_to_peak(self.row(a)).total_cmp(&peak_to_peak(self.row(b))))
132            .and_then(|r| self.channel_ids.get(r).copied())
133    }
134
135    /// Largest peak-to-peak swing across all channels in `mean`.
136    pub fn amplitude(&self) -> f32 {
137        (0..self.num_channels).map(|r| peak_to_peak(self.row(r))).fold(0.0, f32::max)
138    }
139}
140
141// Dense template arrays (`DenseTemplates`, `TemplateAxisOrder`) are the file layout: dspk-io.
142
143/// Packs `(slot, template)` pairs into dense `(mean, std, se)` buffers in `order` (Phy uses the
144/// unit id as slot, Zarr / NWB the table row), on at least `num_channels` channels.
145pub fn pack_templates<'a>(
146    units: impl IntoIterator<Item = (usize, Option<&'a WaveformTemplate>)>,
147    num_channels: usize,
148    order: TemplateAxisOrder,
149) -> Option<([usize; 3], Vec<f32>, Vec<f32>, Vec<f32>)> {
150    let list: Vec<(usize, Option<&WaveformTemplate>)> = units.into_iter().collect();
151    let count = list.iter().map(|&(slot, _)| slot + 1).max().unwrap_or(0);
152    let samples = list.iter().filter_map(|(_, t)| t.map(|w| w.num_samples)).max().unwrap_or(0);
153    let channels = num_channels.max(
154        list.iter()
155            .filter_map(|(_, t)| *t)
156            .flat_map(|t| t.channel_ids.iter().copied())
157            .max()
158            .map_or(0, |c| c + 1),
159    );
160    if count == 0 || samples == 0 || channels == 0 {
161        return None;
162    }
163    let total = count * samples * channels;
164    let mut mean = vec![0.0f32; total];
165    let mut std = vec![0.0f32; total];
166    let mut se = vec![0.0f32; total];
167
168    for (slot, tpl) in list {
169        let Some(t) = tpl else { continue };
170        for (r, &ch) in t.channel_ids.iter().enumerate() {
171            if ch < channels {
172                let n_s = t.num_samples.min(samples);
173                let m_row = t.row(r);
174                let sd_row = t.std_row(r);
175                let se_row = t.se_row(r);
176                for s in 0..n_s {
177                    let idx = match order {
178                        TemplateAxisOrder::SamplesChannels => (slot * samples + s) * channels + ch,
179                        TemplateAxisOrder::ChannelsSamples => (slot * channels + ch) * samples + s,
180                    };
181                    mean[idx] = m_row[s];
182                    std[idx] = sd_row[s];
183                    se[idx] = se_row[s];
184                }
185            }
186        }
187    }
188    let shape = match order {
189        TemplateAxisOrder::SamplesChannels => [count, samples, channels],
190        TemplateAxisOrder::ChannelsSamples => [count, channels, samples],
191    };
192    Some((shape, mean, std, se))
193}
194
195/// Unpacks slot `slot` from dense `(mean, std, se)` buffers in `order` into a [`WaveformTemplate`].
196pub fn unpack_template(
197    slot: usize,
198    shape: [usize; 3],
199    order: TemplateAxisOrder,
200    mean_buf: &[f32],
201    std_buf: Option<&[f32]>,
202    se_buf: Option<&[f32]>,
203    spike_count: usize,
204) -> Option<WaveformTemplate> {
205    let (count, channels, samples) = match order {
206        TemplateAxisOrder::SamplesChannels => (shape[0], shape[2], shape[1]),
207        TemplateAxisOrder::ChannelsSamples => (shape[0], shape[1], shape[2]),
208    };
209    if slot >= count || channels == 0 || samples == 0 {
210        return None;
211    }
212    let at = |buf: &[f32], c: usize, s: usize| match order {
213        TemplateAxisOrder::SamplesChannels => buf[(slot * samples + s) * channels + c],
214        TemplateAxisOrder::ChannelsSamples => buf[(slot * channels + c) * samples + s],
215    };
216    let n = channels * samples;
217    let mut mean = Vec::with_capacity(n);
218    for c in 0..channels {
219        for s in 0..samples {
220            mean.push(at(mean_buf, c, s));
221        }
222    }
223    let std = match std_buf {
224        Some(sd) if sd.len() == mean_buf.len() => {
225            let mut v = Vec::with_capacity(n);
226            for c in 0..channels {
227                for s in 0..samples {
228                    v.push(at(sd, c, s));
229                }
230            }
231            v
232        }
233        _ => vec![1.0; n],
234    };
235    let mut w = WaveformTemplate::with_count((0..channels).collect(), samples, spike_count.max(1), mean, std);
236    if let Some(se) = se_buf.filter(|s| s.len() == mean_buf.len()) {
237        let mut v = Vec::with_capacity(n);
238        for c in 0..channels {
239            for s in 0..samples {
240                v.push(at(se, c, s));
241            }
242        }
243        w.se = v;
244    }
245    Some(w)
246}
247
248/// Template `t` of `dense` as a [`WaveformTemplate`] on the channels where it is not zero.
249pub fn dense_waveform(dense: &DenseTemplates, t: usize) -> WaveformTemplate {
250    let channels: Vec<usize> = (0..dense.channels).filter(|&c| dense.trace(t, c).iter().any(|v| *v != 0.0)).collect();
251    let mean: Vec<f32> = channels.iter().flat_map(|&c| dense.trace(t, c)).collect();
252    let std = vec![0.0; mean.len()];
253    WaveformTemplate::new(channels, dense.samples, mean, std)
254}
255
256/// Degrees-of-freedom correction of template standard deviations: 0 (population SD, numpy's
257/// default), as Phy / SpikeInterface `templates_std`.
258pub const TEMPLATE_STD_DDOF: u64 = 0;
259
260/// Computes the mean waveform template, standard deviation ([`TEMPLATE_STD_DDOF`]) and standard
261/// error (`SE = SD / sqrt(n)`) across a set of aligned waveforms.
262/// Snippets on other channels (or with another shape) than the first one are skipped.
263pub fn compute_mean_template(snippets: &[WaveformSnippet]) -> Option<WaveformTemplate> {
264    let first = snippets.first()?;
265    let n_s = first.num_samples;
266    let total_elements = first.num_channels() * n_s;
267    let used: Vec<&WaveformSnippet> = snippets
268        .iter()
269        .filter(|s| s.waveform.len() == total_elements && s.channel_ids == first.channel_ids)
270        .collect();
271    if used.is_empty() {
272        return None;
273    }
274    let mut moments = RunningMoments::new(total_elements);
275    used.iter().for_each(|s| moments.push(&s.waveform));
276    let mean = moments.mean().iter().map(|&m| m as f32).collect();
277    let std = moments.std(TEMPLATE_STD_DDOF).into_iter().map(|s| s as f32).collect();
278    Some(WaveformTemplate::with_count(first.channel_ids.clone(), n_s, used.len(), mean, std))
279}
280
281/// Every unit's waveform template from labelled peaks, accumulated over windows of a recording on
282/// the device: for each `(unit, channel, sample)` the Welford mean and `Mâ‚‚` of the unit's spikes,
283/// read straight from the window (no snippets are cut). The state stays on the device between
284/// windows (only the per-unit counts are on the host), and Welford's update continues across them,
285/// so any split into windows gives the same templates. Dense: every channel.
286pub struct UnitTemplateAccumulator {
287    client: cubecl::prelude::Client,
288    n_units: usize,
289    channels: usize,
290    n_before: usize,
291    width: usize,
292    counts: Vec<u32>,
293    mean: cubecl::server::Handle,
294    m2: cubecl::server::Handle,
295}
296
297impl UnitTemplateAccumulator {
298    /// Templates of `n_units` units on `channels` channels, `width` samples starting `n_before`
299    /// before each peak.
300    pub fn new(client: &cubecl::prelude::Client, n_units: usize, channels: usize, n_before: usize, width: usize) -> Self {
301        let len = n_units * channels * width;
302        Self {
303            client: client.clone(),
304            n_units,
305            channels,
306            n_before,
307            width,
308            counts: vec![0; n_units],
309            mean: dspk_ops::core::buffer::zeros::<f32>(client, len),
310            m2: dspk_ops::core::buffer::zeros::<f32>(client, len),
311        }
312    }
313
314    /// Adds the spikes of one window: `data` is `[channels, samples]` `f32` on the device, spike
315    /// `i` at sample `peak_samples[i]` of it, of unit `labels[i]` (negative or ≥ `n_units`: left
316    /// out). Callers leave `n_before` / `width − n_before` of margin (past the window, the edge
317    /// sample is read).
318    ///
319    /// # Panics
320    ///
321    /// If `peak_samples` and `labels` differ in length.
322    pub fn add(&mut self, data: &cubecl::server::Handle, samples: usize, peak_samples: &[u32], labels: &[i32]) {
323        use cubecl::prelude::*;
324        use dspk_ops::core::buffer;
325
326        assert_eq!(peak_samples.len(), labels.len(), "one label per peak");
327        let units = self.n_units;
328        // Counting sort of the spikes by unit (spike order kept within a unit)
329        let mut offsets = vec![0u32; units + 1];
330        for &l in labels {
331            if l >= 0 && (l as usize) < units {
332                offsets[l as usize + 1] += 1;
333            }
334        }
335        for u in 0..units {
336            offsets[u + 1] += offsets[u];
337        }
338        if offsets[units] == 0 || self.channels == 0 || self.width == 0 {
339            return;
340        }
341        let mut cursor = offsets.clone();
342        let mut order = vec![0u32; offsets[units] as usize];
343        for (i, &l) in labels.iter().enumerate() {
344            if l >= 0 && (l as usize) < units {
345                order[cursor[l as usize] as usize] = i as u32;
346                cursor[l as usize] += 1;
347            }
348        }
349        let client = &self.client;
350        let total = units * self.channels * self.width;
351        let geom = dspk_core::compute::LaunchGeometry::elementwise(client, total);
352        // SAFETY: every array is passed with the length it was created with
353        unsafe {
354            super::kernels::accumulate_unit_templates_kernel::launch::<f32>(
355                client,
356                geom.cube_count,
357                geom.cube_dim,
358                BufferArg::from_raw_parts(data.clone(), self.channels * samples),
359                BufferArg::from_raw_parts(buffer::upload(client, peak_samples), peak_samples.len()),
360                BufferArg::from_raw_parts(buffer::upload(client, &order), order.len()),
361                BufferArg::from_raw_parts(buffer::upload(client, &offsets), offsets.len()),
362                BufferArg::from_raw_parts(buffer::upload(client, &self.counts), units),
363                BufferArg::from_raw_parts(self.mean.clone(), total),
364                BufferArg::from_raw_parts(self.m2.clone(), total),
365                units as u32,
366                self.channels as u32,
367                samples as u32,
368                self.n_before as u32,
369                self.width as u32,
370            );
371        }
372        for u in 0..units {
373            self.counts[u] += offsets[u + 1] - offsets[u];
374        }
375    }
376
377    /// Spikes accumulated per unit.
378    pub fn counts(&self) -> &[u32] {
379        &self.counts
380    }
381
382    /// The means on the device, `[units, channels, width]` `f32` (e.g. for template matching).
383    pub fn device_mean(&self) -> &cubecl::server::Handle {
384        &self.mean
385    }
386
387    /// Every unit's dense template (`channel_ids` `0..channels`, std with [`TEMPLATE_STD_DDOF`]),
388    /// `None` for a unit without spikes. One download.
389    pub fn templates(&self) -> Vec<Option<WaveformTemplate>> {
390        use dspk_ops::core::buffer;
391
392        let total = self.n_units * self.channels * self.width;
393        let mean = buffer::download_prefix::<f32>(&self.client, self.mean.clone(), total);
394        let m2 = buffer::download_prefix::<f32>(&self.client, self.m2.clone(), total);
395        let per = self.channels * self.width;
396        (0..self.n_units)
397            .map(|u| {
398                let n = self.counts[u] as u64;
399                (n > 0).then(|| {
400                    let denom = n.saturating_sub(TEMPLATE_STD_DDOF).max(1) as f32;
401                    let std = m2[u * per..(u + 1) * per].iter().map(|&v| (v.max(0.0) / denom).sqrt()).collect();
402                    WaveformTemplate::with_count((0..self.channels).collect(), self.width, n as usize, mean[u * per..(u + 1) * per].to_vec(), std)
403                })
404            })
405            .collect()
406    }
407}
408
409#[cfg(test)]
410mod unit_template_tests {
411    use super::*;
412    use dspk_ops::core::buffer;
413    use dspk_core::compute::ComputeTarget;
414
415    /// Two units with known shapes on 3 channels, plus small deterministic noise; spike times on a
416    /// fixed grid. Returns `(data, samples, peak_samples, labels, shapes)`.
417    fn recording() -> (Vec<f32>, usize, Vec<u32>, Vec<i32>, [Vec<f32>; 2]) {
418        let (channels, samples, width, n_before) = (3usize, 40_000usize, 20usize, 5usize);
419        let shapes = [
420            (0..channels * width).map(|i| { let (c, t) = (i / width, i % width); -(10.0 - 3.0 * c as f32) * (-0.5 * ((t as f32 - 5.0) / 2.0).powi(2)).exp() }).collect::<Vec<f32>>(),
421            (0..channels * width).map(|i| { let (c, t) = (i / width, i % width); 4.0 * (c as f32 + 1.0) * (-0.5 * ((t as f32 - 8.0) / 3.0).powi(2)).exp() }).collect::<Vec<f32>>(),
422        ];
423        let mut data: Vec<f32> = (0..channels * samples).map(|i| ((i * 7919 % 101) as f32 - 50.0) / 500.0).collect();
424        let (mut peaks, mut labels) = (Vec::new(), Vec::new());
425        for k in 0..300 {
426            let t0 = 100 + k * 120;
427            let u = k % 3; // 2 = unassigned (label −1): adds signal but not to a template
428            let shape: Vec<f32> = if u < 2 { shapes[u].clone() } else { shapes[0].iter().map(|v| v * 5.0).collect() };
429            for c in 0..channels {
430                for t in 0..width {
431                    data[c * samples + t0 + t] += shape[c * width + t];
432                }
433            }
434            peaks.push((t0 + n_before) as u32);
435            labels.push(if u < 2 { u as i32 } else { -1 });
436        }
437        (data, samples, peaks, labels, shapes)
438    }
439
440    #[test]
441    fn templates_recover_the_units_and_ignore_unassigned_spikes() {
442        let Ok(target) = ComputeTarget::from_env() else { return };
443        let client = target.client().unwrap();
444        let (data, samples, peaks, labels, shapes) = recording();
445        let handle = buffer::upload(&client, &data);
446        let mut acc = UnitTemplateAccumulator::new(&client, 2, 3, 5, 20);
447        acc.add(&handle, samples, &peaks, &labels);
448        assert_eq!(acc.counts(), &[100, 100]);
449        let t = acc.templates();
450        for u in 0..2 {
451            let tmpl = t[u].as_ref().unwrap();
452            assert_eq!((tmpl.count, tmpl.channel_ids.len(), tmpl.num_samples), (100, 3, 20));
453            let err = tmpl.mean.iter().zip(&shapes[u]).map(|(a, b)| (a - b).abs()).fold(0.0f32, f32::max);
454            assert!(err < 0.05, "unit {u}: max error {err}");
455            assert!(tmpl.std.iter().all(|&s| s < 0.2), "unit {u}: std is the noise only");
456        }
457    }
458
459    /// Splitting the spikes into windows gives the same templates as one window.
460    #[test]
461    fn windows_add_up_to_the_whole() {
462        let Ok(target) = ComputeTarget::from_env() else { return };
463        let client = target.client().unwrap();
464        let (data, samples, peaks, labels, _) = recording();
465        let handle = buffer::upload(&client, &data);
466        let mut whole = UnitTemplateAccumulator::new(&client, 2, 3, 5, 20);
467        whole.add(&handle, samples, &peaks, &labels);
468        let mut parts = UnitTemplateAccumulator::new(&client, 2, 3, 5, 20);
469        let half = peaks.len() / 2;
470        parts.add(&handle, samples, &peaks[..half], &labels[..half]);
471        parts.add(&handle, samples, &peaks[half..], &labels[half..]);
472        let (a, b) = (whole.templates(), parts.templates());
473        for u in 0..2 {
474            let (a, b) = (a[u].as_ref().unwrap(), b[u].as_ref().unwrap());
475            assert_eq!(a.count, b.count);
476            assert!(a.mean.iter().zip(&b.mean).all(|(x, y)| (x - y).abs() < 1e-5));
477            assert!(a.std.iter().zip(&b.std).all(|(x, y)| (x - y).abs() < 1e-4));
478        }
479        // A unit without spikes has no template
480        let empty = UnitTemplateAccumulator::new(&client, 3, 3, 5, 20);
481        assert!(empty.templates().iter().all(Option::is_none));
482    }
483}