use dspk_core::DspResult;
use dspk_ops::math::RunningMoments;
use crate::data::events::Event;
use crate::ui::plot::canvas::{Canvas, Frame, Pixel};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignTo {
#[default]
OwnEvents,
AllEvents,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChannelAverage {
pub channel: usize,
pub trials: usize,
pub mean: Vec<f32>,
pub sd: Vec<f32>,
pub snippets: Vec<Vec<f32>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlignedAverage {
pub time_ms: Vec<f64>,
pub channels: Vec<ChannelAverage>,
}
pub fn aligned_average(
read: impl Fn(usize, std::ops::Range<u64>, &mut [f32]) -> DspResult<()>,
total: u64,
rate: f64,
channels: &[usize],
events: &[Event],
align: AlignTo,
pre_s: f64,
post_s: f64,
keep: usize,
) -> DspResult<AlignedAverage> {
let pre = (pre_s * rate).round().max(0.0) as u64;
let post = (post_s * rate).round().max(1.0) as u64;
let len = (pre + post) as usize;
let time_ms = (0..len).map(|i| (i as f64 - pre as f64) * 1e3 / rate).collect();
let mut buf = vec![0.0f32; len];
let mut out = Vec::with_capacity(channels.len());
for &ch in channels {
let mut m = RunningMoments::new(len);
let mut snippets = Vec::new();
for e in events.iter().filter(|e| align == AlignTo::AllEvents || e.channel as usize == ch) {
let at = (e.onset_s * rate).round() as i64;
let (a, b) = (at - pre as i64, at + post as i64);
if a < 0 || b as u64 > total {
continue;
}
read(ch, a as u64..b as u64, &mut buf)?;
m.push(&buf);
if snippets.len() < keep {
snippets.push(buf.clone());
}
}
let trials = m.count() as usize;
out.push(ChannelAverage {
channel: ch,
trials,
mean: m.mean().iter().map(|&v| v as f32).collect(),
sd: m.std(1).into_iter().map(|v| v as f32).collect(),
snippets,
});
}
Ok(AlignedAverage { time_ms, channels: out })
}
pub fn draw(avg: &ChannelAverage, event_frac: f32, width: u32, height: u32, background: Pixel, grid: Pixel, colour: Pixel, scale: f32) -> Frame {
let mut frame = Frame::new(width, height);
let mut c = frame.canvas();
c.pixels.fill(background);
let n = avg.mean.len();
if n < 2 || avg.trials == 0 {
return frame;
}
let snip = avg.snippets.iter().flatten().copied();
let lo = avg.mean.iter().zip(&avg.sd).map(|(m, s)| m - s).chain(snip.clone()).fold(f32::INFINITY, f32::min).min(0.0);
let hi = avg.mean.iter().zip(&avg.sd).map(|(m, s)| m + s).chain(snip).fold(f32::NEG_INFINITY, f32::max).max(0.0);
let span = (hi - lo).max(1e-12);
let (w, h) = (width as f32, height as f32);
let pad = 4.0 * scale;
let y = |v: f32| pad + (1.0 - (v - lo) / span) * (h - 2.0 * pad);
let x = |i: usize| i as f32 / (n - 1) as f32 * (w - 1.0);
let zero_y = y(0.0);
c.rect(0.0, zero_y - 0.5, w, zero_y + 0.5, grid, 0.8);
let ex = event_frac.clamp(0.0, 1.0) * (w - 1.0);
c.rect(ex - 0.5 * scale, 0.0, ex + 0.5 * scale, h, grid, 1.0);
let faint = (0.6 / (avg.snippets.len() as f32).sqrt().max(1.0)).clamp(0.08, 0.35);
for s in &avg.snippets {
let points: Vec<(f32, f32)> = (0..n).map(|i| (x(i), y(s[i]))).collect();
polyline(&mut c, &points, colour, faint, 1.0 * scale);
}
for col in 0..width as usize {
let i = ((col as f32 / (w - 1.0).max(1.0)) * (n - 1) as f32).round() as usize;
let (m, s) = (avg.mean[i], avg.sd[i]);
c.span_aa_alpha(col, y(m + s), y(m - s), colour, 0.22);
}
let points: Vec<(f32, f32)> = (0..n).map(|i| (x(i), y(avg.mean[i]))).collect();
polyline(&mut c, &points, colour, 1.0, 1.5 * scale);
frame
}
pub fn polyline(canvas: &mut Canvas, points: &[(f32, f32)], c: Pixel, alpha: f32, width: f32) {
for w in points.windows(2) {
let ((x0, y0), (x1, y1)) = (w[0], w[1]);
let (xa, xb) = (x0.round() as i64, x1.round() as i64);
for x in xa.max(0)..=xb.max(xa) {
if x as usize >= canvas.width {
break;
}
let f = if xb > xa { (x - xa) as f32 / (xb - xa) as f32 } else { 0.0 };
let f2 = if xb > xa { ((x + 1 - xa) as f32 / (xb - xa) as f32).min(1.0) } else { 1.0 };
let (ya, yb) = (y0 + (y1 - y0) * f, y0 + (y1 - y0) * f2);
canvas.span_aa_alpha(x as usize, ya.min(yb) - width * 0.5, ya.max(yb) + width * 0.5, c, alpha);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn read(ch: usize, r: std::ops::Range<u64>, out: &mut [f32]) -> DspResult<()> {
for (o, s) in out.iter_mut().zip(r) {
*o = if ch == 0 { s as f32 } else { 5.0 };
}
Ok(())
}
#[test]
fn averages_each_channel_around_its_events() {
let ev = |ch: u32, t: f64| Event { channel: ch, onset_s: t, offset_s: None, peak_value: 0.0 };
let events = [ev(0, 0.1), ev(0, 0.3), ev(1, 0.2), ev(0, 0.001)];
let a = aligned_average(read, 1000, 1000.0, &[0, 1], &events, AlignTo::OwnEvents, 0.01, 0.02, 1).unwrap();
assert_eq!(a.channels[0].snippets.len(), 1, "one kept");
assert_eq!(a.time_ms.len(), 30);
assert_eq!((a.time_ms[0], a.time_ms[10]), (-10.0, 0.0));
let c0 = &a.channels[0];
assert_eq!(c0.trials, 2);
assert_eq!(c0.mean[10], 200.0);
assert!((c0.sd[10] - 141.42136).abs() < 1e-3);
assert_eq!(a.channels[1].trials, 1);
let all = aligned_average(read, 1000, 1000.0, &[1], &events, AlignTo::AllEvents, 0.01, 0.02, 0).unwrap();
assert_eq!((all.channels[0].trials, all.channels[0].mean[0], all.channels[0].sd[0]), (3, 5.0, 0.0));
let f = draw(&c0.clone(), 1.0 / 3.0, 120, 40, Pixel::rgb(0, 0, 0), Pixel::rgb(60, 60, 60), Pixel::rgb(250, 200, 50), 1.0);
assert!(f.pixels().iter().any(|p| p.r > 200 && p.g > 150), "the mean is drawn");
}
}