add iOS LSV peak detection mirroring Rust algorithm

This commit is contained in:
jess 2026-04-02 18:15:18 -07:00
parent df6268d2ac
commit 324c8a7f5a
1 changed files with 107 additions and 0 deletions

View File

@ -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..<n {
let lo = max(0, i - half)
let hi = min(n - 1, i + half)
var sum: Float = 0
for j in lo...hi { sum += data[j] }
out[i] = sum / Float(hi - lo + 1)
}
return out
}
func findExtrema(_ v: [Float], _ iSmooth: [Float], minProminence: Float) -> [(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[..<idx]
let rightSlice = iSmooth[(idx + 1)...]
let leftBound: Float
let rightBound: Float
if isMax {
leftBound = leftSlice.min() ?? val
rightBound = rightSlice.min() ?? val
return val - max(leftBound, rightBound) >= 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..<smoothed.count {
let prev = smoothed[i - 1]
let curr = smoothed[i]
if prev.sign != curr.sign && prev != 0 {
let frac = abs(prev) / (abs(prev) + abs(curr))
let vCross = vVals[i - 1] + frac * (vVals[i] - vVals[i - 1])
peaks.append(LsvPeak(vMv: vCross, iUa: 0, kind: .crossover))
break
}
}
// largest peak in positive voltage region -> 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
}