feat: implement Haptika PPG and Watch V1
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
|
||||
public enum WatchBaselineMaturity: String, Codable, Sendable {
|
||||
case unavailable
|
||||
case provisional
|
||||
case personalized
|
||||
}
|
||||
|
||||
public enum WatchBaselineComparison: String, Codable, Sendable {
|
||||
case unavailable
|
||||
case usual
|
||||
case slightlyDifferent
|
||||
case clearlyDifferent
|
||||
}
|
||||
|
||||
public struct WatchBaselineSnapshot: Codable, Equatable, Sendable {
|
||||
public let maturity: WatchBaselineMaturity
|
||||
public let comparableMeasurements: Int
|
||||
public let medianBPM: Double?
|
||||
public let medianAbsoluteDeviation: Double?
|
||||
|
||||
public init(measurements: [WatchMeasurementSummaryV1]) {
|
||||
let values = measurements
|
||||
.filter { $0.source == .appleWatchOptical && $0.isValid && $0.quality >= 0.65 && $0.context.isBaselineComparable }
|
||||
.compactMap(\.averageBPM)
|
||||
.filter(\.isFinite)
|
||||
|
||||
comparableMeasurements = values.count
|
||||
medianBPM = Self.median(values)
|
||||
medianAbsoluteDeviation = medianBPM.map { median in
|
||||
Self.median(values.map { abs($0 - median) }) ?? 0
|
||||
}
|
||||
if values.count >= 10 {
|
||||
maturity = .personalized
|
||||
} else if values.count >= 5 {
|
||||
maturity = .provisional
|
||||
} else {
|
||||
maturity = .unavailable
|
||||
}
|
||||
}
|
||||
|
||||
public func compare(bpm: Double?) -> WatchBaselineComparison {
|
||||
guard maturity != .unavailable, let bpm, bpm.isFinite, let medianBPM else {
|
||||
return .unavailable
|
||||
}
|
||||
let scale = max(medianAbsoluteDeviation ?? 0, 2)
|
||||
let robustDistance = abs(bpm - medianBPM) / scale
|
||||
if robustDistance < 2 { return .usual }
|
||||
if robustDistance < 4 { return .slightlyDifferent }
|
||||
return .clearlyDifferent
|
||||
}
|
||||
|
||||
private static func median(_ values: [Double]) -> Double? {
|
||||
guard !values.isEmpty else { return nil }
|
||||
let sorted = values.sorted()
|
||||
let middle = sorted.count / 2
|
||||
return sorted.count.isMultiple(of: 2)
|
||||
? (sorted[middle - 1] + sorted[middle]) / 2
|
||||
: sorted[middle]
|
||||
}
|
||||
}
|
||||
|
||||
public extension WatchBaselineComparison {
|
||||
var frenchLabel: String {
|
||||
switch self {
|
||||
case .unavailable: "Baseline en construction"
|
||||
case .usual: "Habituel"
|
||||
case .slightlyDifferent: "Légèrement différent"
|
||||
case .clearlyDifferent: "Clairement différent"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user