add LSV peak detection module with smoothing, extrema finding, and classification
This commit is contained in:
parent
1abf46f0c3
commit
e5fe1c9229
|
|
@ -0,0 +1,128 @@
|
||||||
|
use crate::protocol::LsvPoint;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum PeakKind {
|
||||||
|
FreeCl,
|
||||||
|
TotalCl,
|
||||||
|
Crossover,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LsvPeak {
|
||||||
|
pub v_mv: f32,
|
||||||
|
pub i_ua: f32,
|
||||||
|
pub kind: PeakKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn smooth(data: &[f32], window: usize) -> Vec<f32> {
|
||||||
|
let n = data.len();
|
||||||
|
if n == 0 || window < 2 {
|
||||||
|
return data.to_vec();
|
||||||
|
}
|
||||||
|
let half = window / 2;
|
||||||
|
let mut out = Vec::with_capacity(n);
|
||||||
|
for i in 0..n {
|
||||||
|
let lo = i.saturating_sub(half);
|
||||||
|
let hi = (i + half).min(n - 1);
|
||||||
|
let sum: f32 = data[lo..=hi].iter().sum();
|
||||||
|
out.push(sum / (hi - lo + 1) as f32);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_extrema(v: &[f32], i_smooth: &[f32], min_prominence: f32) -> Vec<(usize, bool)> {
|
||||||
|
let n = i_smooth.len();
|
||||||
|
if n < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut candidates: Vec<(usize, bool)> = Vec::new();
|
||||||
|
for i in 1..n - 1 {
|
||||||
|
let prev = i_smooth[i - 1];
|
||||||
|
let curr = i_smooth[i];
|
||||||
|
let next = i_smooth[i + 1];
|
||||||
|
if curr > prev && curr > next {
|
||||||
|
candidates.push((i, true));
|
||||||
|
} else if curr < prev && curr < next {
|
||||||
|
candidates.push((i, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.retain(|&(idx, is_max)| {
|
||||||
|
let val = i_smooth[idx];
|
||||||
|
let left_bound = i_smooth[..idx].iter().copied()
|
||||||
|
.reduce(if is_max { f32::min } else { f32::max })
|
||||||
|
.unwrap_or(val);
|
||||||
|
let right_bound = i_smooth[idx + 1..].iter().copied()
|
||||||
|
.reduce(if is_max { f32::min } else { f32::max })
|
||||||
|
.unwrap_or(val);
|
||||||
|
let prom = if is_max {
|
||||||
|
val - left_bound.max(right_bound)
|
||||||
|
} else {
|
||||||
|
left_bound.min(right_bound) - val
|
||||||
|
};
|
||||||
|
prom >= min_prominence
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = v; // voltage array available for future use
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_peaks(points: &[LsvPoint]) -> Vec<LsvPeak> {
|
||||||
|
if points.len() < 5 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let i_vals: Vec<f32> = points.iter().map(|p| p.i_ua).collect();
|
||||||
|
let v_vals: Vec<f32> = points.iter().map(|p| p.v_mv).collect();
|
||||||
|
|
||||||
|
let window = 5.max(points.len() / 50);
|
||||||
|
let smoothed = smooth(&i_vals, window);
|
||||||
|
|
||||||
|
let i_min = smoothed.iter().copied().fold(f32::INFINITY, f32::min);
|
||||||
|
let i_max = smoothed.iter().copied().fold(f32::NEG_INFINITY, f32::max);
|
||||||
|
let prominence = (i_max - i_min) * 0.05;
|
||||||
|
|
||||||
|
let extrema = find_extrema(&v_vals, &smoothed, prominence);
|
||||||
|
|
||||||
|
let mut peaks: Vec<LsvPeak> = Vec::new();
|
||||||
|
|
||||||
|
// find crossover: where current changes sign
|
||||||
|
for i in 1..smoothed.len() {
|
||||||
|
if smoothed[i - 1].signum() != smoothed[i].signum() && smoothed[i - 1].signum() != 0.0 {
|
||||||
|
let frac = smoothed[i - 1].abs() / (smoothed[i - 1].abs() + smoothed[i].abs());
|
||||||
|
let v_cross = v_vals[i - 1] + frac * (v_vals[i] - v_vals[i - 1]);
|
||||||
|
peaks.push(LsvPeak {
|
||||||
|
v_mv: v_cross,
|
||||||
|
i_ua: 0.0,
|
||||||
|
kind: PeakKind::Crossover,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// largest peak in positive voltage region -> FreeCl
|
||||||
|
let free_cl = extrema.iter()
|
||||||
|
.filter(|&&(idx, is_max)| is_max && v_vals[idx] >= 0.0)
|
||||||
|
.max_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
if let Some(&(idx, _)) = free_cl {
|
||||||
|
peaks.push(LsvPeak {
|
||||||
|
v_mv: v_vals[idx],
|
||||||
|
i_ua: smoothed[idx],
|
||||||
|
kind: PeakKind::FreeCl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// largest peak in negative voltage region -> TotalCl
|
||||||
|
let total_cl = extrema.iter()
|
||||||
|
.filter(|&&(idx, is_max)| is_max && v_vals[idx] < 0.0)
|
||||||
|
.max_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
if let Some(&(idx, _)) = total_cl {
|
||||||
|
peaks.push(LsvPeak {
|
||||||
|
v_mv: v_vals[idx],
|
||||||
|
i_ua: smoothed[idx],
|
||||||
|
kind: PeakKind::TotalCl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
peaks
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod app;
|
mod app;
|
||||||
|
mod lsv_analysis;
|
||||||
mod native_menu;
|
mod native_menu;
|
||||||
mod plot;
|
mod plot;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue