import Foundation enum PeakKind { case freeCl, totalCl, crossover } struct LsvPeak { var vMv: Float var iUa: Float var kind: PeakKind } func smoothLsv(_ data: [Float], window: Int) -> [Float] { let n = data.count if n == 0 || window < 2 { return data } let half = window / 2 var out = [Float](repeating: 0, count: n) for i in 0.. [(Int, Bool)] { let n = iSmooth.count if n < 3 { return [] } var candidates: [(Int, Bool)] = [] for i in 1..<(n - 1) { let prev = iSmooth[i - 1] let curr = iSmooth[i] let next = iSmooth[i + 1] if curr > prev && curr > next { candidates.append((i, true)) } else if curr < prev && curr < next { candidates.append((i, false)) } } return candidates.filter { (idx, isMax) in let val = iSmooth[idx] let leftSlice = iSmooth[..= minProminence } else { leftBound = leftSlice.max() ?? val rightBound = rightSlice.max() ?? val return min(leftBound, rightBound) - val >= minProminence } } } func detectLsvPeaks(_ points: [LsvPoint]) -> [LsvPeak] { if points.count < 5 { return [] } let iVals = points.map { $0.iUa } let vVals = points.map { $0.vMv } let window = max(5, points.count / 50) let smoothed = smoothLsv(iVals, window: window) guard let iMin = smoothed.min(), let iMax = smoothed.max() else { return [] } let prominence = (iMax - iMin) * 0.05 let extrema = findExtrema(vVals, smoothed, minProminence: prominence) var peaks: [LsvPeak] = [] // crossover: where current changes sign for i in 1.. freeCl let freeCl = extrema .filter { $0.1 && vVals[$0.0] >= 0 } .max(by: { smoothed[$0.0] < smoothed[$1.0] }) if let (idx, _) = freeCl { peaks.append(LsvPeak(vMv: vVals[idx], iUa: smoothed[idx], kind: .freeCl)) } // largest peak in negative voltage region -> totalCl let totalCl = extrema .filter { $0.1 && vVals[$0.0] < 0 } .max(by: { smoothed[$0.0] < smoothed[$1.0] }) if let (idx, _) = totalCl { peaks.append(LsvPeak(vMv: vVals[idx], iUa: smoothed[idx], kind: .totalCl)) } return peaks }