Skip to content

Commit fefd994

Browse files
authored
Merge pull request #999 from LoopKit/retrospective-correction-refactor
Refactor Retrospective Correction to allow for future enhancements
2 parents 01fd7c7 + 76e0bac commit fefd994

File tree

8 files changed

+182
-76
lines changed

8 files changed

+182
-76
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */; };
7777
434FB6461D68F1CD007B9C70 /* Amplitude.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 434FB6451D68F1CD007B9C70 /* Amplitude.framework */; };
7878
434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */; };
79+
43511CE221FD80E400566C63 /* RetrospectiveCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43511CDF21FD80E400566C63 /* RetrospectiveCorrection.swift */; };
80+
43511CE321FD80E400566C63 /* StandardRetrospectiveCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43511CE021FD80E400566C63 /* StandardRetrospectiveCorrection.swift */; };
7981
43511CEE220FC61700566C63 /* HUDRowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43511CED220FC61700566C63 /* HUDRowController.swift */; };
8082
43523EDB1CC35083001850F1 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43523EDA1CC35083001850F1 /* RileyLinkKit.framework */; };
8183
435400311C9F744E00D5819C /* BolusSuggestionUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */; };
@@ -617,6 +619,8 @@
617619
434FB6451D68F1CD007B9C70 /* Amplitude.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Amplitude.framework; sourceTree = BUILT_PRODUCTS_DIR; };
618620
434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = "<group>"; };
619621
434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; };
622+
43511CDF21FD80E400566C63 /* RetrospectiveCorrection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetrospectiveCorrection.swift; sourceTree = "<group>"; };
623+
43511CE021FD80E400566C63 /* StandardRetrospectiveCorrection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardRetrospectiveCorrection.swift; sourceTree = "<group>"; };
620624
43511CED220FC61700566C63 /* HUDRowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDRowController.swift; sourceTree = "<group>"; };
621625
43523EDA1CC35083001850F1 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
622626
435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusSuggestionUserInfo.swift; sourceTree = "<group>"; };
@@ -1202,9 +1206,19 @@
12021206
path = Display;
12031207
sourceTree = "<group>";
12041208
};
1209+
43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */ = {
1210+
isa = PBXGroup;
1211+
children = (
1212+
43511CDF21FD80E400566C63 /* RetrospectiveCorrection.swift */,
1213+
43511CE021FD80E400566C63 /* StandardRetrospectiveCorrection.swift */,
1214+
);
1215+
path = RetrospectiveCorrection;
1216+
sourceTree = "<group>";
1217+
};
12051218
43757D131C06F26C00910CB9 /* Models */ = {
12061219
isa = PBXGroup;
12071220
children = (
1221+
43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */,
12081222
43880F961D9D8052009061A8 /* ServiceAuthentication */,
12091223
C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */,
12101224
43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */,
@@ -2458,6 +2472,7 @@
24582472
43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */,
24592473
C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */,
24602474
43FCEEA9221A615B0013DD30 /* StatusChartsManager.swift in Sources */,
2475+
43511CE321FD80E400566C63 /* StandardRetrospectiveCorrection.swift in Sources */,
24612476
435400321C9F745500D5819C /* BolusSuggestionUserInfo.swift in Sources */,
24622477
43E3449F1B9D68E900C85C07 /* StatusTableViewController.swift in Sources */,
24632478
43DBF0531C93EC8200B3C386 /* DeviceDataManager.swift in Sources */,
@@ -2518,6 +2533,7 @@
25182533
4372E490213CFCE70068E043 /* LoopSettingsUserInfo.swift in Sources */,
25192534
89CA2B3D226E6B13004D9350 /* LocalTestingScenariosManager.swift in Sources */,
25202535
43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */,
2536+
43511CE221FD80E400566C63 /* RetrospectiveCorrection.swift in Sources */,
25212537
438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */,
25222538
892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */,
25232539
4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */,

Loop/Managers/LoopDataManager.swift

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ final class LoopDataManager {
8181

8282
glucoseStore = GlucoseStore(healthStore: healthStore, cacheStore: cacheStore, cacheLength: .hours(24))
8383

84+
retrospectiveCorrection = settings.enabledRetrospectiveCorrectionAlgorithm
85+
8486
overrideHistory.delegate = self
8587
cacheStore.delegate = self
8688

@@ -218,6 +220,9 @@ final class LoopDataManager {
218220
}
219221
}
220222

223+
// Confined to dataAccessQueue
224+
private var retrospectiveCorrection: RetrospectiveCorrection
225+
221226
// MARK: - Background task management
222227

223228
private var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
@@ -647,7 +652,7 @@ extension LoopDataManager {
647652
throw LoopError.missingDataError(.glucose)
648653
}
649654

650-
let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-settings.retrospectiveCorrectionIntegrationInterval)
655+
let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-retrospectiveCorrection.retrospectionInterval)
651656

652657
let earliestEffectDate = Date(timeIntervalSinceNow: .hours(-24))
653658
let nextEffectDate = insulinCounteractionEffects.last?.endDate ?? earliestEffectDate
@@ -827,39 +832,38 @@ extension LoopDataManager {
827832
return prediction
828833
}
829834

830-
/// Generates an effect based on how large the discrepancy is between the current glucose and its predicted value.
835+
/// Generates a correction effect based on how large the discrepancy is between the current glucose and its model predicted value.
831836
///
832-
/// - Parameter effectDuration: The length of time to extend the effect
833837
/// - Throws: LoopError.missingDataError
834-
private func updateRetrospectiveGlucoseEffect(effectDuration: TimeInterval = TimeInterval(minutes: 60)) throws {
838+
private func updateRetrospectiveGlucoseEffect() throws {
835839
dispatchPrecondition(condition: .onQueue(dataAccessQueue))
836840

841+
// Get carb effects, otherwise clear effect and throw error
837842
guard let carbEffects = self.carbEffect else {
838843
retrospectiveGlucoseDiscrepancies = nil
839844
retrospectiveGlucoseEffect = []
840845
throw LoopError.missingDataError(.carbEffect)
841846
}
842847

843-
retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta)
844-
845-
// Our last change should be recent, otherwise clear the effects
846-
guard let discrepancy = retrospectiveGlucoseDiscrepanciesSummed?.last,
847-
Date().timeIntervalSince(discrepancy.endDate) <= settings.recencyInterval
848-
else {
849-
retrospectiveGlucoseEffect = []
850-
return
851-
}
852-
848+
// Get most recent glucose, otherwise clear effect and throw error
853849
guard let glucose = self.glucoseStore.latestGlucose else {
854850
retrospectiveGlucoseEffect = []
855851
throw LoopError.missingDataError(.glucose)
856852
}
857853

858-
let unit = HKUnit.milligramsPerDeciliter
859-
let discrepancyTime = max(discrepancy.endDate.timeIntervalSince(discrepancy.startDate), settings.retrospectiveCorrectionGroupingInterval)
860-
let velocity = HKQuantity(unit: unit.unitDivided(by: .second()), doubleValue: discrepancy.quantity.doubleValue(for: unit) / discrepancyTime)
854+
// Get timeline of glucose discrepancies
855+
retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta)
861856

862-
retrospectiveGlucoseEffect = glucose.decayEffect(atRate: velocity, for: effectDuration)
857+
// Calculate retrospective correction
858+
retrospectiveGlucoseEffect = retrospectiveCorrection.computeEffect(
859+
startingAt: glucose,
860+
retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
861+
recencyInterval: settings.recencyInterval,
862+
insulinSensitivitySchedule: insulinSensitivitySchedule,
863+
basalRateSchedule: basalRateSchedule,
864+
glucoseCorrectionRangeSchedule: settings.glucoseTargetRangeSchedule,
865+
retrospectiveCorrectionGroupingInterval: settings.retrospectiveCorrectionGroupingInterval
866+
)
863867
}
864868

865869
/// Runs the glucose prediction on the latest effect data.
@@ -1026,6 +1030,9 @@ protocol LoopState {
10261030
/// The difference in predicted vs actual glucose over a recent period
10271031
var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? { get }
10281032

1033+
/// The total corrective glucose effect from retrospective correction
1034+
var totalRetrospectiveCorrection: HKQuantity? { get }
1035+
10291036
/// Calculates a new prediction from the current data using the specified effect inputs
10301037
///
10311038
/// This method is intended for visualization purposes only, not dosing calculation. No validation of input data is done.
@@ -1087,6 +1094,11 @@ extension LoopDataManager {
10871094
return loopDataManager.retrospectiveGlucoseDiscrepanciesSummed
10881095
}
10891096

1097+
var totalRetrospectiveCorrection: HKQuantity? {
1098+
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
1099+
return loopDataManager.retrospectiveCorrection.totalGlucoseCorrectionEffect
1100+
}
1101+
10901102
func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] {
10911103
return try loopDataManager.predictGlucose(using: inputs)
10921104
}
@@ -1158,7 +1170,7 @@ extension LoopDataManager {
11581170

11591171
"retrospectiveGlucoseDiscrepancies: [",
11601172
"* GlucoseEffect(start, mg/dL)",
1161-
(manager.retrospectiveGlucoseDiscrepancies ?? []).reduce(into: "", { (entries, entry) in
1173+
(state.retrospectiveGlucoseDiscrepancies ?? []).reduce(into: "", { (entries, entry) in
11621174
entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n")
11631175
}),
11641176
"]",
@@ -1182,6 +1194,8 @@ extension LoopDataManager {
11821194
"",
11831195
"cacheStore: \(String(reflecting: self.glucoseStore.cacheStore))",
11841196
"",
1197+
String(reflecting: self.retrospectiveCorrection),
1198+
"",
11851199
]
11861200

11871201
self.glucoseStore.generateDiagnosticReport { (report) in
@@ -1206,7 +1220,7 @@ extension LoopDataManager {
12061220

12071221

12081222
extension Notification.Name {
1209-
static let LoopDataUpdated = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopDataUpdated")
1223+
static let LoopDataUpdated = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopDataUpdated")
12101224

12111225
static let LoopRunning = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopRunning")
12121226
}

Loop/Models/LoopSettings+Loop.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Copyright © 2018 LoopKit Authors. All rights reserved.
66
//
77

8+
import Foundation
89
import LoopCore
910

1011
// MARK: - Static configuration
@@ -16,4 +17,11 @@ extension LoopSettings {
1617
}
1718
return inputs
1819
}
20+
21+
static let retrospectiveCorrectionEffectDuration = TimeInterval(hours: 1)
22+
23+
/// Creates an instance of the enabled retrospective correction implementation
24+
var enabledRetrospectiveCorrectionAlgorithm: RetrospectiveCorrection {
25+
return StandardRetrospectiveCorrection(effectDuration: LoopSettings.retrospectiveCorrectionEffectDuration)
26+
}
1927
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// RetrospectiveCorrection.swift
3+
// Loop
4+
//
5+
// Copyright © 2019 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import HealthKit
10+
import LoopKit
11+
12+
13+
/// Derives a continued glucose effect from recent prediction discrepancies
14+
protocol RetrospectiveCorrection: CustomDebugStringConvertible {
15+
/// The maximum interval of historical glucose discrepancies that should be provided to the computation
16+
var retrospectionInterval: TimeInterval { get }
17+
18+
/// Overall retrospective correction effect
19+
var totalGlucoseCorrectionEffect: HKQuantity? { get }
20+
21+
/// Calculates overall correction effect based on timeline of discrepancies, and updates glucoseCorrectionEffect
22+
///
23+
/// - Parameters:
24+
/// - glucose: Most recent glucose
25+
/// - retrospectiveGlucoseDiscrepanciesSummed: Timeline of past discepancies
26+
/// - Returns: Glucose correction effects
27+
func computeEffect(
28+
startingAt startingGlucose: GlucoseValue,
29+
retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?,
30+
recencyInterval: TimeInterval,
31+
insulinSensitivitySchedule: InsulinSensitivitySchedule?,
32+
basalRateSchedule: BasalRateSchedule?,
33+
glucoseCorrectionRangeSchedule: GlucoseRangeSchedule?,
34+
retrospectiveCorrectionGroupingInterval: TimeInterval
35+
) -> [GlucoseEffect]
36+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// StandardRetrospectiveCorrection.swift
3+
// Loop
4+
//
5+
// Created by Dragan Maksimovic on 10/27/18.
6+
// Copyright © 2018 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import HealthKit
11+
import LoopKit
12+
13+
/**
14+
Standard Retrospective Correction (RC) calculates a correction effect in glucose prediction based on the most recent discrepancy between observed glucose movement and movement expected based on insulin and carb models. Standard retrospective correction acts as a proportional (P) controller aimed at reducing modeling errors in glucose prediction.
15+
16+
In the above summary, "discrepancy" is a difference between the actual glucose and the model predicted glucose over retrospective correction grouping interval (set to 30 min in LoopSettings)
17+
*/
18+
class StandardRetrospectiveCorrection: RetrospectiveCorrection {
19+
let retrospectionInterval = TimeInterval(minutes: 30)
20+
21+
/// RetrospectiveCorrection protocol variables
22+
/// Standard effect duration
23+
let effectDuration: TimeInterval
24+
/// Overall retrospective correction effect
25+
var totalGlucoseCorrectionEffect: HKQuantity?
26+
27+
/// All math is performed with glucose expressed in mg/dL
28+
private let unit = HKUnit.milligramsPerDeciliter
29+
30+
init(effectDuration: TimeInterval) {
31+
self.effectDuration = effectDuration
32+
}
33+
34+
func computeEffect(
35+
startingAt startingGlucose: GlucoseValue,
36+
retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?,
37+
recencyInterval: TimeInterval,
38+
insulinSensitivitySchedule: InsulinSensitivitySchedule?,
39+
basalRateSchedule: BasalRateSchedule?,
40+
glucoseCorrectionRangeSchedule: GlucoseRangeSchedule?,
41+
retrospectiveCorrectionGroupingInterval: TimeInterval
42+
) -> [GlucoseEffect] {
43+
// Last discrepancy should be recent, otherwise clear the effect and return
44+
let glucoseDate = startingGlucose.startDate
45+
guard let currentDiscrepancy = retrospectiveGlucoseDiscrepanciesSummed?.last,
46+
glucoseDate.timeIntervalSince(currentDiscrepancy.endDate) <= recencyInterval
47+
else {
48+
totalGlucoseCorrectionEffect = nil
49+
return []
50+
}
51+
52+
// Standard retrospective correction math
53+
let currentDiscrepancyValue = currentDiscrepancy.quantity.doubleValue(for: unit)
54+
totalGlucoseCorrectionEffect = HKQuantity(unit: unit, doubleValue: currentDiscrepancyValue)
55+
56+
let retrospectionTimeInterval = currentDiscrepancy.endDate.timeIntervalSince(currentDiscrepancy.startDate)
57+
let discrepancyTime = max(retrospectionTimeInterval, retrospectiveCorrectionGroupingInterval)
58+
let velocity = HKQuantity(unit: unit.unitDivided(by: .second()), doubleValue: currentDiscrepancyValue / discrepancyTime)
59+
60+
// Update array of glucose correction effects
61+
return startingGlucose.decayEffect(atRate: velocity, for: effectDuration)
62+
}
63+
64+
var debugDescription: String {
65+
let report: [String] = [
66+
"## StandardRetrospectiveCorrection",
67+
""
68+
]
69+
70+
return report.joined(separator: "\n")
71+
}
72+
}

0 commit comments

Comments
 (0)