diff --git a/cue/src/lsv_analysis.rs b/cue/src/lsv_analysis.rs index 0fe8ba9..b6bc244 100644 --- a/cue/src/lsv_analysis.rs +++ b/cue/src/lsv_analysis.rs @@ -91,6 +91,69 @@ pub fn detect_qhq_peak(points: &[LsvPoint]) -> Option { .map(|&(idx, _)| v_vals[idx]) } +#[derive(Debug, Clone, Copy)] +pub struct ClPotentials { + pub v_free: f32, + pub v_free_detected: bool, + pub v_total: f32, + pub v_total_detected: bool, +} + +pub fn derive_cl_potentials(points: &[LsvPoint]) -> ClPotentials { + let default = ClPotentials { + v_free: 100.0, + v_free_detected: false, + v_total: -200.0, + v_total_detected: false, + }; + if points.len() < 5 { + return default; + } + + let i_vals: Vec = points.iter().map(|p| p.i_ua).collect(); + let v_vals: Vec = 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); + + // v_free: most prominent cathodic peak (is_max==false) in +300 to -300 mV + let free_peak = extrema.iter() + .filter(|&&(idx, is_max)| !is_max && v_vals[idx] >= -300.0 && v_vals[idx] <= 300.0) + .min_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal)); + + let (v_free, v_free_detected, free_idx) = match free_peak { + Some(&(idx, _)) => (v_vals[idx], true, Some(idx)), + None => (100.0, false, None), + }; + + // v_total: secondary cathodic peak between (v_free - 100) and -500 mV, excluding free peak + let total_lo = -500.0_f32; + let total_hi = v_free - 100.0; + let total_peak = extrema.iter() + .filter(|&&(idx, is_max)| { + !is_max + && v_vals[idx] >= total_lo + && v_vals[idx] <= total_hi + && Some(idx) != free_idx + }) + .min_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal)); + + let (v_total, v_total_detected) = match total_peak { + Some(&(idx, _)) => (v_vals[idx], true), + None => (v_free - 300.0, false), + }; + + let v_total = v_total.max(-400.0); + + ClPotentials { v_free, v_free_detected, v_total, v_total_detected } +} + pub fn detect_peaks(points: &[LsvPoint]) -> Vec { if points.len() < 5 { return Vec::new();