diff --git a/cue-ios/CueIOS/Models/LsvAnalysis.swift b/cue-ios/CueIOS/Models/LsvAnalysis.swift new file mode 100644 index 0000000..f436219 --- /dev/null +++ b/cue-ios/CueIOS/Models/LsvAnalysis.swift @@ -0,0 +1,107 @@ +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 +}