mod device;
pub mod kernels;
use crate::error::{OpsError, OpsResult};
pub use device::detect_crossings_on_device;
fn invalid(msg: String) -> OpsError {
OpsError::Core(dspk_core::DspError::InvalidConfig(msg))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
Rising,
Falling,
Both,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CrossingOptions {
pub levels: Vec<f32>,
pub releases: Vec<f32>,
pub direction: Direction,
pub min_duration_samples: u64,
pub refractory_samples: u64,
}
impl CrossingOptions {
pub fn new(level: f32) -> Self {
Self { levels: vec![level], releases: Vec::new(), direction: Direction::Rising, min_duration_samples: 0, refractory_samples: 0 }
}
fn sides(&self, channels: usize) -> OpsResult<Vec<Side>> {
let spread = |v: &[f32], what: &str| -> OpsResult<Vec<f32>> {
match v.len() {
1 => Ok(vec![v[0]; channels]),
n if n == channels => Ok(v.to_vec()),
n => Err(invalid(format!("crossings: {n} {what} for {channels} channels (give one, or one per channel)"))),
}
};
let levels = spread(&self.levels, "levels")?;
let releases = if self.releases.is_empty() { levels.clone() } else { spread(&self.releases, "releases")? };
for (ch, (&l, &r)) in levels.iter().zip(&releases).enumerate() {
if !l.is_finite() || !r.is_finite() {
return Err(invalid(format!("crossings: channel {ch}: level and release must be finite")));
}
let (bad, rule) = match self.direction {
Direction::Rising => (r > l, "at most"),
Direction::Falling => (r < l, "at least"),
Direction::Both => (r.abs() > l.abs(), "in magnitude at most"),
};
if bad {
return Err(invalid(format!("crossings: channel {ch}: the release ({r}) must be {rule} the level ({l})")));
}
}
let side = |sign: f32, magnitude: bool| {
let to = |v: &f32| if magnitude { v.abs() } else { sign * v };
Side::new(sign, levels.iter().map(to).collect(), releases.iter().map(to).collect())
};
Ok(match self.direction {
Direction::Rising => vec![side(1.0, false)],
Direction::Falling => vec![side(-1.0, false)],
Direction::Both => vec![side(1.0, true), side(-1.0, true)],
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CrossingEvent {
pub channel: usize,
pub onset: u64,
pub offset: Option<u64>,
pub peak_sample: u64,
pub peak_value: f32,
}
#[derive(Debug, Clone, Copy)]
struct ChannelState {
on: bool,
onset: u64,
peak: f32,
peak_at: u64,
ready_at: u64,
}
#[derive(Debug, Clone)]
struct Side {
sign: f32,
levels: Vec<f32>,
releases: Vec<f32>,
state: Vec<ChannelState>,
}
impl Side {
fn new(sign: f32, levels: Vec<f32>, releases: Vec<f32>) -> Self {
let idle = ChannelState { on: false, onset: 0, peak: f32::NEG_INFINITY, peak_at: 0, ready_at: 0 };
let n = levels.len();
Self { sign, levels, releases, state: vec![idle; n] }
}
}
#[derive(Debug, Clone)]
pub struct CrossingTracker {
sides: Vec<Side>,
min_duration: u64,
refractory: u64,
prev: Vec<f32>,
events: Vec<CrossingEvent>,
}
type Crossings<'a> = &'a [(u32, u32)];
type Stretches<'a> = &'a [(f32, u32)];
impl CrossingTracker {
pub fn new(channels: usize, options: &CrossingOptions) -> OpsResult<Self> {
Ok(Self {
sides: options.sides(channels)?,
min_duration: options.min_duration_samples,
refractory: options.refractory_samples,
prev: vec![f32::NAN; channels],
events: Vec::new(),
})
}
pub fn channels(&self) -> usize {
self.prev.len()
}
fn take(&mut self, side: usize, ch: usize, origin: u64, crossings: Crossings, stretches: Stretches) {
debug_assert_eq!(stretches.len(), crossings.len() + 1);
let sign = self.sides[side].sign;
let st = &mut self.sides[side].state[ch];
let grow = |st: &mut ChannelState, (v, t): (f32, u32)| {
if v > st.peak {
(st.peak, st.peak_at) = (v, origin + t as u64);
}
};
if st.on {
grow(st, stretches[0]);
}
for (k, &(t, kind)) in crossings.iter().enumerate() {
let at = origin + t as u64;
let after = stretches[k + 1];
if kind == kernels::CROSSING_UP {
if st.on {
grow(st, after);
} else if at >= st.ready_at {
(st.on, st.onset, st.peak, st.peak_at) = (true, at, f32::NEG_INFINITY, at);
grow(st, after);
}
} else if st.on {
st.on = false;
if at - st.onset >= self.min_duration {
self.events.push(CrossingEvent { channel: ch, onset: st.onset, offset: Some(at), peak_sample: st.peak_at, peak_value: sign * st.peak });
st.ready_at = at + self.refractory;
}
}
}
}
pub fn push_host_window(&mut self, rows: &[f32], samples: usize, scan: std::ops::Range<usize>, origin: u64) {
let mut crossings = Vec::new();
let mut stretches = Vec::new();
for side in 0..self.sides.len() {
for ch in 0..self.channels() {
let row = &rows[ch * samples..(ch + 1) * samples];
let Side { sign: s, ref levels, ref releases, .. } = self.sides[side];
let (l, r) = (levels[ch], releases[ch]);
crossings.clear();
for t in scan.clone() {
let p = if t > 0 { row[t - 1] } else { self.prev[ch] };
let (sp, sv) = (s * p, s * row[t]);
if sp < l && sv >= l {
crossings.push((t as u32, kernels::CROSSING_UP));
} else if sp >= r && sv < r {
crossings.push((t as u32, kernels::CROSSING_DOWN));
}
}
stretches.clear();
let bounds: Vec<u32> = std::iter::once(scan.start as u32).chain(crossings.iter().map(|c| c.0)).chain(std::iter::once(scan.end as u32)).collect();
for w in bounds.windows(2) {
let mut best = (-3.0e38f32, w[0]);
for t in w[0]..w[1] {
let v = s * row[t as usize];
if v > best.0 {
best = (v, t);
}
}
stretches.push(best);
}
self.take(side, ch, origin, &crossings, &stretches);
}
}
if scan.end > 0 {
for ch in 0..self.channels() {
self.prev[ch] = rows[ch * samples + scan.end - 1];
}
}
}
pub fn finish(mut self) -> Vec<CrossingEvent> {
for side in &self.sides {
for (ch, st) in side.state.iter().enumerate() {
if st.on {
self.events.push(CrossingEvent { channel: ch, onset: st.onset, offset: None, peak_sample: st.peak_at, peak_value: side.sign * st.peak });
}
}
}
self.events.sort_by(|a, b| (a.onset, a.channel).cmp(&(b.onset, b.channel)).then(a.peak_value.total_cmp(&b.peak_value)));
self.events
}
}
pub fn detect_crossings(rows: &[f32], channels: usize, samples: usize, options: &CrossingOptions) -> OpsResult<Vec<CrossingEvent>> {
if rows.len() != channels * samples {
return Err(invalid(format!("crossings: {} values for {channels} × {samples}", rows.len())));
}
let mut tracker = CrossingTracker::new(channels, options)?;
tracker.push_host_window(rows, samples, 0..samples, 0);
Ok(tracker.finish())
}
#[cfg(test)]
mod tests {
use super::*;
fn signal() -> (Vec<f32>, usize) {
let n = 40;
let mut x = vec![0.0f32; 2 * n];
for (t, v) in [(5, 6.0), (6, 9.0), (7, 7.0), (8, 1.0), (20, 8.0), (21, 4.0), (22, 10.0), (23, 6.0)] {
x[t] = v;
}
for t in 30..35 {
x[n + t] = 3.0 + t as f32 - 30.0;
}
(x, n)
}
#[test]
fn events_with_hysteresis_and_peaks() {
let (x, n) = signal();
let mut o = CrossingOptions::new(5.0);
o.releases = vec![2.0];
let ev = detect_crossings(&x, 2, n, &o).unwrap();
let got: Vec<_> = ev.iter().map(|e| (e.channel, e.onset, e.offset, e.peak_sample, e.peak_value)).collect();
assert_eq!(got, vec![(0, 5, Some(8), 6, 9.0), (0, 20, Some(24), 22, 10.0), (1, 32, Some(35), 34, 7.0)]);
let ev = detect_crossings(&x, 2, n, &CrossingOptions::new(5.0)).unwrap();
assert_eq!(ev.iter().filter(|e| e.channel == 0).count(), 3);
}
#[test]
fn min_duration_refractory_and_open_events() {
let (x, n) = signal();
let mut o = CrossingOptions::new(5.0);
o.min_duration_samples = 4;
let ev = detect_crossings(&x, 2, n, &o).unwrap();
assert!(ev.iter().all(|e| e.offset.unwrap() - e.onset >= 4), "{ev:?}");
let mut o = CrossingOptions::new(5.0);
o.releases = vec![2.0];
o.refractory_samples = 20;
let ev = detect_crossings(&x[..n], 1, n, &o).unwrap();
assert_eq!(ev.len(), 1, "the second bump starts within 20 samples of the first's end");
let ev = detect_crossings(&x[..21], 1, 21, &CrossingOptions::new(5.0)).unwrap();
assert_eq!(ev.last().map(|e| (e.onset, e.offset)), Some((20, None)));
}
#[test]
fn falling_and_chunks_give_the_same_events() {
let (x, n) = signal();
let neg: Vec<f32> = x.iter().map(|v| -v).collect();
let mut o = CrossingOptions::new(-5.0);
o.releases = vec![-2.0];
o.direction = Direction::Falling;
let whole = detect_crossings(&neg, 2, n, &o).unwrap();
assert_eq!(whole.iter().map(|e| e.peak_value).collect::<Vec<_>>(), vec![-9.0, -10.0, -7.0]);
let mut t = CrossingTracker::new(2, &o).unwrap();
for start in (0..n).step_by(7) {
let end = (start + 7).min(n);
let w = end - start;
let rows: Vec<f32> = (0..2).flat_map(|c| neg[c * n + start..c * n + end].to_vec()).collect();
t.push_host_window(&rows, w, 0..w, start as u64);
}
assert_eq!(t.finish(), whole);
}
#[test]
fn both_directions_at_once() {
let (x, n) = signal();
let mut y = x[..n].to_vec();
for t in 30..33 {
y[t] = -8.0;
}
let mut o = CrossingOptions::new(5.0);
o.releases = vec![2.0];
o.direction = Direction::Both;
let ev = detect_crossings(&y, 1, n, &o).unwrap();
let got: Vec<_> = ev.iter().map(|e| (e.onset, e.offset, e.peak_value)).collect();
assert_eq!(got, vec![(5, Some(8), 9.0), (20, Some(24), 10.0), (30, Some(33), -8.0)]);
o.levels = vec![-5.0];
assert_eq!(detect_crossings(&y, 1, n, &o).unwrap(), ev);
}
#[test]
fn bad_options_are_refused() {
assert!(detect_crossings(&[0.0; 4], 2, 2, &CrossingOptions { levels: vec![1.0, 2.0, 3.0], ..CrossingOptions::new(0.0) }).is_err());
let mut o = CrossingOptions::new(5.0);
o.releases = vec![6.0];
assert!(detect_crossings(&[0.0; 4], 2, 2, &o).is_err(), "a rising release above the level");
}
}