Skip to content

Commit caaacfc

Browse files
committed
Refactor Retrospective Correction to allow for future enhancements
1 parent 9e68991 commit caaacfc

File tree

6 files changed

+74
-76
lines changed

6 files changed

+74
-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
}

Loop/View Controllers/PredictionTableViewController.swift

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
6868

6969
private var retrospectiveGlucoseDiscrepancies: [GlucoseChange]?
7070

71+
private var totalRetrospectiveCorrection: HKQuantity?
72+
7173
private var refreshContext = RefreshContext.all
7274

7375
private var chartStartDate: Date {
@@ -105,6 +107,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
105107

106108
let reloadGroup = DispatchGroup()
107109
var glucoseValues: [StoredGlucoseSample]?
110+
var totalRetrospectiveCorrection: HKQuantity?
108111

109112
if self.refreshContext.remove(.glucose) != nil {
110113
reloadGroup.enter()
@@ -119,6 +122,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
119122
reloadGroup.enter()
120123
self.deviceManager.loopManager.getLoopState { (manager, state) in
121124
self.retrospectiveGlucoseDiscrepancies = state.retrospectiveGlucoseDiscrepancies
125+
totalRetrospectiveCorrection = state.totalRetrospectiveCorrection
122126
self.glucoseChart.setPredictedGlucoseValues(state.predictedGlucose ?? [])
123127

124128
do {
@@ -148,6 +152,10 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
148152
}
149153
self.charts.invalidateChart(atIndex: 0)
150154

155+
if let totalRetrospectiveCorrection = totalRetrospectiveCorrection {
156+
self.totalRetrospectiveCorrection = totalRetrospectiveCorrection
157+
}
158+
151159
self.charts.prerender()
152160

153161
self.tableView.beginUpdates()
@@ -173,12 +181,9 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
173181

174182
// MARK: - UITableViewDataSource
175183

176-
private enum Section: Int {
184+
private enum Section: Int, CaseIterable {
177185
case charts
178186
case inputs
179-
case settings
180-
181-
static let count = 3
182187
}
183188

184189
private var eventualGlucoseDescription: String?
@@ -188,7 +193,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
188193
private var selectedInputs = PredictionInputEffect.all
189194

190195
override func numberOfSections(in tableView: UITableView) -> Int {
191-
return Section.count
196+
return Section.allCases.count
192197
}
193198

194199
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -197,8 +202,6 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
197202
return 1
198203
case .inputs:
199204
return availableInputs.count
200-
case .settings:
201-
return 1
202205
}
203206
}
204207

@@ -221,17 +224,6 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
221224
case .inputs:
222225
let cell = tableView.dequeueReusableCell(withIdentifier: PredictionInputEffectTableViewCell.className, for: indexPath) as! PredictionInputEffectTableViewCell
223226
self.tableView(tableView, updateTextFor: cell, at: indexPath)
224-
return cell
225-
case .settings:
226-
let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell
227-
228-
cell.titleLabel?.text = NSLocalizedString("Enable Retrospective Correction", comment: "Title of the switch which toggles retrospective correction effects")
229-
cell.subtitleLabel?.text = NSLocalizedString("This will more aggresively increase or decrease basal delivery when glucose movement doesn't match the carbohydrate and insulin-based model.", comment: "The description of the switch which toggles retrospective correction effects")
230-
cell.`switch`?.isOn = deviceManager.loopManager.settings.retrospectiveCorrectionEnabled
231-
cell.`switch`?.addTarget(self, action: #selector(retrospectiveCorrectionSwitchChanged(_:)), for: .valueChanged)
232-
233-
cell.contentView.layoutMargins.left = tableView.separatorInset.left
234-
235227
return cell
236228
}
237229
}
@@ -257,7 +249,6 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
257249

258250
cell.titleLabel?.text = input.localizedTitle
259251
cell.accessoryType = selectedInputs.contains(input) ? .checkmark : .none
260-
cell.enabled = input != .retrospection || deviceManager.loopManager.settings.retrospectiveCorrectionEnabled
261252

262253
var subtitleText = input.localizedDescription(forGlucoseUnit: glucoseChart.glucoseUnit) ?? ""
263254

@@ -270,35 +261,27 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
270261
let predicted = HKQuantity(unit: glucoseChart.glucoseUnit, doubleValue: currentGlucose.quantity.doubleValue(for: glucoseChart.glucoseUnit) - lastDiscrepancy.quantity.doubleValue(for: glucoseChart.glucoseUnit))
271262
var values = [predicted, currentGlucose.quantity].map { formatter.string(from: $0, for: glucoseChart.glucoseUnit) ?? "?" }
272263
formatter.numberFormatter.positivePrefix = formatter.numberFormatter.plusSign
273-
values.append(formatter.string(from: lastDiscrepancy.quantity, for: glucoseChart.glucoseUnit) ?? "?" )
264+
values.append(formatter.string(from: lastDiscrepancy.quantity, for: glucoseChart.glucoseUnit) ?? "?")
274265

275266
let retro = String(
276267
format: NSLocalizedString("prediction-description-retrospective-correction", comment: "Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference)"),
277268
values[0], values[1], values[2]
278269
)
279270

271+
// Standard retrospective correction
280272
subtitleText = String(format: "%@\n%@", subtitleText, retro)
281273
}
282274

283275
cell.subtitleLabel?.text = subtitleText
284276
}
285277

286-
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
287-
switch Section(rawValue: section)! {
288-
case .settings:
289-
return NSLocalizedString("Algorithm Settings", comment: "The title of the section containing algorithm settings")
290-
default:
291-
return nil
292-
}
293-
}
294-
295278
// MARK: - UITableViewDelegate
296279

297280
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
298281
switch Section(rawValue: indexPath.section)! {
299282
case .charts:
300283
return 275
301-
case .inputs, .settings:
284+
case .inputs:
302285
return 60
303286
}
304287
}
@@ -320,16 +303,4 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
320303
refreshContext.update(with: .status)
321304
reloadData()
322305
}
323-
324-
// MARK: - Actions
325-
326-
@objc private func retrospectiveCorrectionSwitchChanged(_ sender: UISwitch) {
327-
deviceManager.loopManager.settings.retrospectiveCorrectionEnabled = sender.isOn
328-
329-
if let row = availableInputs.index(where: { $0 == .retrospection }),
330-
let cell = tableView.cellForRow(at: IndexPath(row: row, section: Section.inputs.rawValue)) as? PredictionInputEffectTableViewCell
331-
{
332-
cell.enabled = self.deviceManager.loopManager.settings.retrospectiveCorrectionEnabled
333-
}
334-
}
335306
}

0 commit comments

Comments
 (0)