Skip to content

Commit

Permalink
Supplemental Metrics for ECG Reading (#47)
Browse files Browse the repository at this point in the history
# Supplemental Metrics for ECG Reading

## ♻️ Current situation & Problem
Building on top of #40, this pull request seeks to package ECG readings
with supplemental information like VO2 max and pulse rate in a way that
is sensitive to the possible limitations of current recording
capabilities.


## ⚙️ Release Notes 
- Measures most recent VO2 max known at the time of ECG recording.
- Captures pulse rate via optical sensor at T-minus five minutes and
T-plus five minutes.
- Captures activity metrics at T-minus five minutes and T-plus five
minutes, inlcuding:
  - Active calories.
  - Step count.
  - Standing minutes.
- Collects readings of heart rate from optical sensor at intervals of 10
minutes for first two days of the study.
- Estimates amount of time watch has been worn each day.


## 📚 Documentation
In-line documentation will be written in relevant files in conformance
to the [Spezi Documentation
Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).


## ✅ Testing
Tests may be added for ensuring that data is correctly serialized and
properly handled.


### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
MatthewTurk247 committed Feb 19, 2024
1 parent 78f8287 commit 2ace230
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 2 deletions.
107 changes: 107 additions & 0 deletions PAWS/ECGRecordings/ECGRecording.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,110 @@ struct ECGRecording: View {
}
}
}

extension HKElectrocardiogram {
private var oneDayPredicate: NSPredicate {
HKQuery.predicateForSamples(
withStart: Calendar.current.date(byAdding: .day, value: -1, to: self.startDate), // 24 hours before recording.
end: self.startDate,
options: .strictStartDate
)
}

private var fiveMinutePredicate: NSPredicate {
HKQuery.predicateForSamples(
withStart: Calendar.current.date(byAdding: .minute, value: -5, to: self.startDate), // 5 minutes before recording.
end: self.startDate,
options: .strictStartDate
)
}

var precedingPulseRates: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.heartRate))
}
}

var precedingVo2Max: HKQuantitySample? {
get async throws {
try await precedingSamples(
forType: HKQuantityType(.vo2Max),
sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)],
limit: 1
)
.first
}
}

var precedingPhysicalEffort: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.physicalEffort))
}
}

var precedingStepCount: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.stepCount))
}
}

var precedingActiveEnergy: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.activeEnergyBurned))
}
}

private func precedingSamples(
forType type: HKSampleType,
sortDescriptors: [SortDescriptor<HKSample>] = [SortDescriptor(\.startDate)],
limit: Int? = nil
) async throws -> [HKSample] {
let store = HKHealthStore()
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [.sample(type: type, predicate: self.fiveMinutePredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

// If something is available in last 5 minutes since recording, return those samples.
if let result = try? await queryDescriptor.result(for: store), !result.isEmpty {
return result
}

// Otherwise, request the last 24 hours of samples.
let extendedQueryDescriptor = HKSampleQueryDescriptor(
predicates: [.sample(type: type, predicate: self.oneDayPredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

return try await extendedQueryDescriptor.result(for: store)
}

private func precedingSamples(
forType type: HKQuantityType,
sortDescriptors: [SortDescriptor<HKQuantitySample>] = [SortDescriptor(\.startDate)],
limit: Int? = nil
) async throws -> [HKQuantitySample] {
let store = HKHealthStore()
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: type, predicate: self.fiveMinutePredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

// If something is available in last 5 minutes since recording, return those samples.
if let result = try? await queryDescriptor.result(for: store), !result.isEmpty {
return result
}

// Otherwise, request the last 24 hours of samples.
let extendedQueryDescriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: type, predicate: self.oneDayPredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

return try await extendedQueryDescriptor.result(for: store)
}
}
29 changes: 27 additions & 2 deletions PAWS/PAWSDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class PAWSDelegate: SpeziAppDelegate {
// Collection starts at the time the user consents and lasts for 1 month.
let sharedPredicate = HKQuery.predicateForSamples(
withStart: healthKitStartDate,
end: Calendar.current.date(byAdding: DateComponents(month: 1), to: healthKitStartDate),
end: Calendar.current.date(byAdding: DateComponents(month: 6), to: healthKitStartDate),
options: .strictEndDate
)

Expand All @@ -90,7 +90,32 @@ class PAWSDelegate: SpeziAppDelegate {
CollectSamples(
Set(HKElectrocardiogram.correlatedSymptomTypes),
predicate: sharedPredicate,
deliverySetting: .background(saveAnchor: false)
deliverySetting: .background(saveAnchor: true)
)
CollectSample(
HKQuantityType(.heartRate),
predicate: sharedPredicate,
deliverySetting: .background(saveAnchor: true)
)
CollectSample(
HKQuantityType(.vo2Max),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
CollectSample(
HKQuantityType(.physicalEffort),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
CollectSample(
HKQuantityType(.stepCount),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
CollectSample(
HKQuantityType(.activeEnergyBurned),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
}
}
Expand Down
23 changes: 23 additions & 0 deletions PAWS/PAWSStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,43 @@ actor PAWSStandard: Standard, EnvironmentAccessible, HealthKitConstraint, Onboar


func add(sample: HKSample) async {
var supplementalMetrics: [HKSample] = []

if let hkElectrocardiogram = sample as? HKElectrocardiogram {
ecgStorage.hkElectrocardiograms.append(hkElectrocardiogram)

do {
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingPulseRates)
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingPhysicalEffort)
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingStepCount)
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingActiveEnergy)

if let precedingVo2Max = try await hkElectrocardiogram.precedingVo2Max {
supplementalMetrics.append(precedingVo2Max)
}
} catch {
logger.log("Could not access HealthKit sample: \(error)")
}
}

if let mockWebService {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
let jsonRepresentation = (try? String(data: encoder.encode(sample.resource), encoding: .utf8)) ?? ""
try? await mockWebService.upload(path: "healthkit/\(sample.uuid.uuidString)", body: jsonRepresentation)

for metric in supplementalMetrics {
try? await mockWebService.upload(path: "healthkit/\(metric.uuid.uuidString)", body: (try? String(data: encoder.encode(metric.resource), encoding: .utf8)) ?? "")
}

return
}

do {
try await healthKitDocument(id: sample.id).setData(from: sample.resource)
for metric in supplementalMetrics {
try await healthKitDocument(id: sample.id).setData(from: metric.resource)
}
} catch {
logger.error("Could not store HealthKit sample: \(error)")
}
Expand Down

0 comments on commit 2ace230

Please sign in to comment.