diff --git a/DoseMathTests/Info.plist b/DoseMathTests/Info.plist index 6d86f6a299..b8493765dc 100644 --- a/DoseMathTests/Info.plist +++ b/DoseMathTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleSignature ???? CFBundleVersion diff --git a/Loop Status Extension/Info.plist b/Loop Status Extension/Info.plist index 9f091d68bf..fc0a625ed3 100644 --- a/Loop Status Extension/Info.plist +++ b/Loop Status Extension/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleVersion $(CURRENT_PROJECT_VERSION) AppGroupIdentifier diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 573e187130..d323cb65de 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -2055,7 +2055,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer: loudnate@gmail.com (XZN842LDLT)"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 49; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2122,7 +2122,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer: loudnate@gmail.com (XZN842LDLT)"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 49; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2364,11 +2364,11 @@ CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 49; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 48; + DYLIB_CURRENT_VERSION = 49; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = LoopUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -2391,11 +2391,11 @@ CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 49; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 48; + DYLIB_CURRENT_VERSION = 49; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = LoopUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index 88101cb1b5..846df16c58 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -169,7 +169,8 @@ extension UserDefaults { maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus, suspendThreshold: suspendThreshold, - retrospectiveCorrectionEnabled: bool(forKey: "com.loudnate.Loop.RetrospectiveCorrectionEnabled") + retrospectiveCorrectionEnabled: bool(forKey: "com.loudnate.Loop.RetrospectiveCorrectionEnabled"), + integralRetrospectiveCorrectionEnabled: bool(forKey: "com.loopkit.Loop.IntegralRetrospectiveCorrectionEnabled") ) self.loopSettings = settings diff --git a/Loop/Info.plist b/Loop/Info.plist index e318f38873..051d03f7b7 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleSignature ???? CFBundleURLTypes diff --git a/Loop/Managers/AnalyticsManager.swift b/Loop/Managers/AnalyticsManager.swift index fe08839154..df7770e74b 100644 --- a/Loop/Managers/AnalyticsManager.swift +++ b/Loop/Managers/AnalyticsManager.swift @@ -127,6 +127,10 @@ final class AnalyticsManager: IdentifiableClass { if newValue.retrospectiveCorrectionEnabled != oldValue.retrospectiveCorrectionEnabled { logEvent("Retrospective correction enabled change") } + + if newValue.integralRetrospectiveCorrectionEnabled != oldValue.integralRetrospectiveCorrectionEnabled { + logEvent("Integral retrospective correction enabled change") + } } // MARK: - Loop Events diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index ec59fbf726..08207c9e64 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -38,9 +38,9 @@ final class LoopDataManager { private let logger: CategoryLogger - var glucoseUpdated: Bool // flag used to decide if integral RC should be updated or not - var overallRetrospectiveCorrection: HKQuantity // value used to display overall RC effect to the user - var integralRectrospectiveCorrectionIndicator: String // display integral RC status + fileprivate var glucoseUpdated: Bool // flag used to decide if integral RC should be updated or not + fileprivate var initializeIntegralRetrospectiveCorrection: Bool // flag used to decide if integral RC should be initialized upon Loop relaunch or for other reasons + var overallRetrospectiveCorrection: HKQuantity? // value used to display overall RC effect to the user init( delegate: LoopDataManagerDelegate, @@ -60,8 +60,8 @@ final class LoopDataManager { self.lastTempBasal = lastTempBasal self.settings = settings self.glucoseUpdated = false - self.overallRetrospectiveCorrection = HKQuantity(unit: HKUnit.milligramsPerDeciliter(), doubleValue: 0) - self.integralRectrospectiveCorrectionIndicator = " " + self.initializeIntegralRetrospectiveCorrection = true + self.overallRetrospectiveCorrection = nil let healthStore = HKHealthStore() @@ -483,7 +483,73 @@ final class LoopDataManager { guard let lastGlucoseDate = latestGlucoseDate else { throw LoopError.missingDataError(details: "Glucose data not available", recovery: "Check your CGM data source") } - + + // Reinitialize integral retrospective correction states based on past 60 minutes of data + // For now, do this only once upon Loop relaunch + if self.initializeIntegralRetrospectiveCorrection { + self.initializeIntegralRetrospectiveCorrection = false + let retrospectiveRestartDate = lastGlucoseDate.addingTimeInterval(TimeInterval(minutes: -60)) + + // get insulin effects over the retrospective restart interval + updateGroup.enter() + doseStore.getGlucoseEffects(start: retrospectiveRestartDate.addingTimeInterval(TimeInterval(minutes: -60))) { (result) -> Void in + switch result { + case .failure(let error): + self.logger.error(error) + self.insulinEffect = nil + case .success(let effects): + self.insulinEffect = effects + } + updateGroup.leave() + } + + // get carb effects over the retrospective restart interval + updateGroup.enter() + carbStore.getGlucoseEffects( + start: retrospectiveRestartDate.addingTimeInterval(TimeInterval(minutes: -60)), + effectVelocities: settings.dynamicCarbAbsorptionEnabled ? insulinCounteractionEffects : nil + ) { (result) -> Void in + switch result { + case .failure(let error): + self.logger.error(error) + self.carbEffect = nil + case .success(let effects): + self.carbEffect = effects + } + updateGroup.leave() + } + + _ = updateGroup.wait(timeout: .distantFuture) + + var sampleGlucoseChangeEnd: Date = retrospectiveRestartDate + while sampleGlucoseChangeEnd <= lastGlucoseDate { + self.retrospectiveGlucoseChange = nil + let sampleGlucoseChangeStart = sampleGlucoseChangeEnd.addingTimeInterval(TimeInterval(minutes: -30)) + updateGroup.enter() + self.glucoseStore.getGlucoseChange(start: sampleGlucoseChangeStart, end: sampleGlucoseChangeEnd) { (change) in + self.retrospectiveGlucoseChange = change + updateGroup.leave() + } + + _ = updateGroup.wait(timeout: .distantFuture) + + // do updateRetrospectiveGlucoseEffect() for change during restart interval + self.glucoseUpdated = true + do { + try updateRetrospectiveGlucoseEffect() + } catch let error { + logger.error(error) + } + self.glucoseUpdated = false + + sampleGlucoseChangeEnd = sampleGlucoseChangeEnd.addingTimeInterval(TimeInterval(minutes: 5)) + } + + self.insulinEffect = nil + self.carbEffect = nil + self.retrospectiveGlucoseChange = nil + } + let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-glucoseStore.reflectionDataInterval) if retrospectiveGlucoseChange == nil { @@ -761,7 +827,7 @@ final class LoopDataManager { /** Retrospective correction math, including proportional and integral action */ - struct retrospectiveCorrection { + fileprivate struct RetrospectiveCorrection { let discrepancyGain: Double let persistentDiscrepancyGain: Double @@ -769,7 +835,6 @@ final class LoopDataManager { let integralGain: Double let integralForget: Double let proportionalGain: Double - let carbEffectLimit: Double static var effectDuration: Double = 60 static var previousDiscrepancy: Double = 0 @@ -779,7 +844,6 @@ final class LoopDataManager { discrepancyGain = 1.0 // high-frequency RC gain, equivalent to Loop 1.5 gain = 1 persistentDiscrepancyGain = 5.0 // low-frequency RC gain for persistent errors, must be >= discrepancyGain correctionTimeConstant = 90.0 // correction filter time constant in minutes - carbEffectLimit = 30.0 // reset integral RC if carbEffect over past 30 min is greater than carbEffectLimit expressed in mg/dL let sampleTime: Double = 5.0 // sample time = 5 min integralForget = exp( -sampleTime / correctionTimeConstant ) // must be between 0 and 1 integralGain = ((1 - integralForget) / integralForget) * @@ -790,38 +854,39 @@ final class LoopDataManager { positiveLimit: Double, negativeLimit: Double, carbEffect: Double, + carbEffectLimit: Double, glucoseUpdated: Bool) -> Double { - if (retrospectiveCorrection.previousDiscrepancy * discrepancy < 0 || + if (RetrospectiveCorrection.previousDiscrepancy * discrepancy < 0 || (discrepancy > 0 && carbEffect > carbEffectLimit)){ // reset integral action when discrepancy reverses polarity or // if discrepancy is positive and carb effect is greater than carbEffectLimit - retrospectiveCorrection.effectDuration = 60.0 - retrospectiveCorrection.previousDiscrepancy = 0.0 - retrospectiveCorrection.integralDiscrepancy = integralGain * discrepancy + RetrospectiveCorrection.effectDuration = 60.0 + RetrospectiveCorrection.previousDiscrepancy = 0.0 + RetrospectiveCorrection.integralDiscrepancy = integralGain * discrepancy } else { - if (glucoseUpdated) { + if glucoseUpdated { // update integral action via low-pass filter y[n] = forget * y[n-1] + gain * u[n] - retrospectiveCorrection.integralDiscrepancy = - integralForget * retrospectiveCorrection.integralDiscrepancy + + RetrospectiveCorrection.integralDiscrepancy = + integralForget * RetrospectiveCorrection.integralDiscrepancy + integralGain * discrepancy // impose safety limits on integral retrospective correction - retrospectiveCorrection.integralDiscrepancy = min(max(retrospectiveCorrection.integralDiscrepancy, negativeLimit), positiveLimit) - retrospectiveCorrection.previousDiscrepancy = discrepancy + RetrospectiveCorrection.integralDiscrepancy = min(max(RetrospectiveCorrection.integralDiscrepancy, negativeLimit), positiveLimit) + RetrospectiveCorrection.previousDiscrepancy = discrepancy // extend duration of retrospective correction effect by 10 min, up to a maxium of 180 min - retrospectiveCorrection.effectDuration = - min(retrospectiveCorrection.effectDuration + 10, 180) + RetrospectiveCorrection.effectDuration = + min(RetrospectiveCorrection.effectDuration + 10, 180) } } - let overallDiscrepancy = proportionalGain * discrepancy + retrospectiveCorrection.integralDiscrepancy + let overallDiscrepancy = proportionalGain * discrepancy + RetrospectiveCorrection.integralDiscrepancy return(overallDiscrepancy) } func updateEffectDuration() -> Double { - return(retrospectiveCorrection.effectDuration) + return(RetrospectiveCorrection.effectDuration) } func resetRetrospectiveCorrection() { - retrospectiveCorrection.effectDuration = 60.0 - retrospectiveCorrection.previousDiscrepancy = 0.0 - retrospectiveCorrection.integralDiscrepancy = 0.0 + RetrospectiveCorrection.effectDuration = 60.0 + RetrospectiveCorrection.previousDiscrepancy = 0.0 + RetrospectiveCorrection.integralDiscrepancy = 0.0 return } } @@ -845,15 +910,14 @@ final class LoopDataManager { // integral retrospective correction variables var dynamicEffectDuration: TimeInterval = effectDuration - let RC = retrospectiveCorrection() + let retrospectiveCorrection = RetrospectiveCorrection() guard let change = retrospectiveGlucoseChange else { - // reset integral action variables in case of calibration event - RC.resetRetrospectiveCorrection() dynamicEffectDuration = effectDuration - NSLog("myLoop --- suspected calibration event, no retrospective correction") + self.overallRetrospectiveCorrection = nil + self.glucoseUpdated = false self.retrospectivePredictedGlucose = nil - return // Expected case for calibrations + return // Expected case for calibrations, skip retrospective correction } // Run a retrospective prediction over the duration of the recorded glucose change, using the current carb and insulin effects @@ -867,29 +931,44 @@ final class LoopDataManager { self.retrospectivePredictedGlucose = retrospectivePrediction guard let lastGlucose = retrospectivePrediction.last else { - RC.resetRetrospectiveCorrection() - NSLog("myLoop --- glucose data missing, reset retrospective correction") - return } + retrospectiveCorrection.resetRetrospectiveCorrection() + self.overallRetrospectiveCorrection = nil + self.glucoseUpdated = false + self.retrospectivePredictedGlucose = nil + return // missing glucose data, reset integral RC and skip retrospective correction + } + + let retrospectionTimeInterval = change.end.endDate.timeIntervalSince(change.start.endDate).minutes + if retrospectionTimeInterval < 6 { + self.overallRetrospectiveCorrection = nil + self.glucoseUpdated = false + self.retrospectivePredictedGlucose = nil + return // too few glucose values, erroneous insulin and carb effects, skip retrospective correction + } + let glucoseUnit = HKUnit.milligramsPerDeciliter() let velocityUnit = glucoseUnit.unitDivided(by: HKUnit.second()) - // user settings + // get user settings relevant for calculation of integral retrospective correction safety parameters guard let glucoseTargetRange = settings.glucoseTargetRangeSchedule, let insulinSensitivity = insulinSensitivitySchedule, let basalRates = basalRateSchedule, let suspendThreshold = settings.suspendThreshold?.quantity, - let currentBG = glucoseStore.latestGlucose?.quantity.doubleValue(for: glucoseUnit) + let carbRatio = carbRatioSchedule else { - RC.resetRetrospectiveCorrection() - NSLog("myLoop --- could not get settings, reset retrospective correction") - return - } - let date = Date() - let currentSensitivity = insulinSensitivity.quantity(at: date).doubleValue(for: glucoseUnit) - let currentBasalRate = basalRates.value(at: date) - let currentMinTarget = glucoseTargetRange.minQuantity(at: date).doubleValue(for: glucoseUnit) + retrospectiveCorrection.resetRetrospectiveCorrection() + self.overallRetrospectiveCorrection = nil + self.glucoseUpdated = false + self.retrospectivePredictedGlucose = nil + return // could not get user settings, reset RC and skip retrospective correction + } + let currentBG = change.end.quantity.doubleValue(for: glucoseUnit) + let currentSensitivity = insulinSensitivity.quantity(at: endDate).doubleValue(for: glucoseUnit) + let currentBasalRate = basalRates.value(at: endDate) + let currentCarbRatio = carbRatio.value(at: endDate) + let currentMinTarget = glucoseTargetRange.minQuantity(at: endDate).doubleValue(for: glucoseUnit) let currentSuspendThreshold = suspendThreshold.doubleValue(for: glucoseUnit) // safety limit for + integral action: ISF * (2 hours) * (basal rate) @@ -906,44 +985,46 @@ final class LoopDataManager { let retrospectiveCarbEffect = LoopMath.predictGlucose(change.start, effects: carbEffect.filterDateRange(startDate, endDate)) guard let lastCarbOnlyGlucose = retrospectiveCarbEffect.last else { - RC.resetRetrospectiveCorrection() - NSLog("myLoop --- could not get carb effect, reset retrospective correction") - return + retrospectiveCorrection.resetRetrospectiveCorrection() + self.overallRetrospectiveCorrection = nil + self.glucoseUpdated = false + self.retrospectivePredictedGlucose = nil + return // could not get carb effect, reset integral RC and skip retrospective correction } + + // setup an upper safety limit for carb action + // integral RC resets to standard RC if discrepancy > 0 and carb action is greater than carbEffectLimit in mg/dL over 30 minutes let currentCarbEffect = -change.start.quantity.doubleValue(for: glucoseUnit) + lastCarbOnlyGlucose.quantity.doubleValue(for: glucoseUnit) + let scaledCarbEffect = currentCarbEffect * 30.0 / retrospectionTimeInterval // current carb effect over 30 minutes + let carbEffectLimit: Double = min( 200 * currentCarbRatio / currentSensitivity, 45 ) // [mg/dL] over 30 minutes + // the above line may be replaced by a fixed value if so desired + // let carbEffectLimit = 30 was used during early IRC testing, 15 was found by some to work better for kids + // let carbEffectLimit = 0 is the most conservative setting // update overall retrospective correction - let overallRC = RC.updateRetrospectiveCorrection( + let overallRC = retrospectiveCorrection.updateRetrospectiveCorrection( discrepancy: currentDiscrepancy, positiveLimit: integralActionPositiveLimit, negativeLimit: integralActionNegativeLimit, - carbEffect: currentCarbEffect, - glucoseUpdated: glucoseUpdated + carbEffect: scaledCarbEffect, + carbEffectLimit: carbEffectLimit, + glucoseUpdated: self.glucoseUpdated ) - let effectMinutes = RC.updateEffectDuration() - dynamicEffectDuration = TimeInterval(minutes: effectMinutes) - - // update effect value for display - overallRetrospectiveCorrection = HKQuantity(unit: glucoseUnit, doubleValue: overallRC) - // update integral RC status indicator - if (overallRC - currentDiscrepancy > 0.5) { - integralRectrospectiveCorrectionIndicator = " ⬆️" + let effectMinutes = retrospectiveCorrection.updateEffectDuration() + var scaledDiscrepancy = currentDiscrepancy + if settings.integralRetrospectiveCorrectionEnabled { + // retrospective correction including integral action + scaledDiscrepancy = overallRC * 60.0 / effectMinutes // scaled to account for extended effect duration + dynamicEffectDuration = TimeInterval(minutes: effectMinutes) + self.overallRetrospectiveCorrection = HKQuantity(unit: glucoseUnit, doubleValue: overallRC) } else { - if (overallRC - currentDiscrepancy < -0.5) { - integralRectrospectiveCorrectionIndicator = " ⬇️" - } else { - integralRectrospectiveCorrectionIndicator = " " - } + // standard retrospective correction + dynamicEffectDuration = effectDuration + self.overallRetrospectiveCorrection = HKQuantity(unit: glucoseUnit, doubleValue: currentDiscrepancy) } - - // retrospective correction including integral action - let scaledDiscrepancy = overallRC * 60.0 / effectMinutes // scaled to account for extended effect duration - - // Velocity calculation had change.end.endDate.timeIntervalSince(change.0.endDate) in the denominator, - // which could lead to too high RC gain when retrospection interval is shorter than 30min - // Changed to safe fixed default retrospection interval of 30*60 = 1800 seconds + // Velocity denominator set to safe fixed default retrospection interval of 30*60 = 1800 seconds let velocity = HKQuantity(unit: velocityUnit, doubleValue: scaledDiscrepancy / 1800.0) let type = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodGlucose)! let glucose = HKQuantitySample(type: type, quantity: change.end.quantity, start: change.end.startDate, end: change.end.endDate) @@ -952,25 +1033,34 @@ final class LoopDataManager { // retrospective insulin effect (just for monitoring RC operation) let retrospectiveInsulinEffect = LoopMath.predictGlucose(change.start, effects: insulinEffect.filterDateRange(startDate, endDate)) - guard let lastInsulinOnlyGlucose = retrospectiveInsulinEffect.last else { return } + guard let lastInsulinOnlyGlucose = retrospectiveInsulinEffect.last else { + self.glucoseUpdated = false + return + } let currentInsulinEffect = -change.start.quantity.doubleValue(for: glucoseUnit) + lastInsulinOnlyGlucose.quantity.doubleValue(for: glucoseUnit) - // retrospective delta BG (just for monitoring RC operation) + // retrospective average delta BG (just for monitoring RC operation) let currentDeltaBG = change.end.quantity.doubleValue(for: glucoseUnit) - change.start.quantity.doubleValue(for: glucoseUnit)// mg/dL - // monitoring of retrospective correction in debugger or Console ("message: myLoop") - NSLog("myLoop ******************************************") - NSLog("myLoop ---retrospective correction ([mg/dL] bg unit)---") - NSLog("myLoop Current BG: %f", currentBG) - NSLog("myLoop 30-min retrospective delta BG: %f", currentDeltaBG) - NSLog("myLoop Retrospective insulin effect: %f", currentInsulinEffect) - NSLog("myLoop Retrospectve carb effect: %f", currentCarbEffect) - NSLog("myLoop Current discrepancy: %f", currentDiscrepancy) - NSLog("myLoop Overall retrospective correction: %f", overallRC) - NSLog("myLoop Correction effect duration [min]: %f", effectMinutes) + if self.glucoseUpdated { + // monitoring of retrospective correction in debugger or Console ("message: myLoop") + NSLog("myLoop ******************************************") + NSLog("myLoop ---retrospective correction ([mg/dL] bg unit)---") + NSLog("myLoop Integral retrospective correction enabled: %d", settings.integralRetrospectiveCorrectionEnabled) + NSLog("myLoop Current BG: %f", currentBG) + NSLog("myLoop 30-min retrospective delta BG: %4.2f", currentDeltaBG) + NSLog("myLoop Retrospective insulin effect: %4.2f", currentInsulinEffect) + NSLog("myLoop Retrospectve carb effect: %4.2f", currentCarbEffect) + NSLog("myLoop Scaled carb effect: %4.2f", scaledCarbEffect) + NSLog("myLoop Carb effect limit: %4.2f", carbEffectLimit) + NSLog("myLoop Current discrepancy: %4.2f", currentDiscrepancy) + NSLog("myLoop Retrospection time interval: %4.2f", retrospectionTimeInterval) + NSLog("myLoop Overall retrospective correction: %4.2f", overallRC) + NSLog("myLoop Correction effect duration [min]: %4.2f", effectMinutes) + } - glucoseUpdated = false // prevent further integral RC updates unless glucose has been updated + glucoseUpdated = false // ensure we are only updating integral RC once per BG update } /// Measure the effects counteracting insulin observed in the CGM glucose. diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index 24d374a153..7c35e638a3 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -23,6 +23,8 @@ struct LoopSettings { var suspendThreshold: GlucoseThreshold? = nil var retrospectiveCorrectionEnabled = true + + var integralRetrospectiveCorrectionEnabled = true } @@ -74,13 +76,18 @@ extension LoopSettings: RawRepresentable { if let retrospectiveCorrectionEnabled = rawValue["retrospectiveCorrectionEnabled"] as? Bool { self.retrospectiveCorrectionEnabled = retrospectiveCorrectionEnabled } + + if let integralRetrospectiveCorrectionEnabled = rawValue["integralRetrospectiveCorrectionEnabled"] as? Bool { + self.integralRetrospectiveCorrectionEnabled = integralRetrospectiveCorrectionEnabled + } } var rawValue: RawValue { var raw: RawValue = [ "version": LoopSettings.version, "dosingEnabled": dosingEnabled, - "retrospectiveCorrectionEnabled": retrospectiveCorrectionEnabled + "retrospectiveCorrectionEnabled": retrospectiveCorrectionEnabled, + "integralRetrospectiveCorrectionEnabled": integralRetrospectiveCorrectionEnabled ] raw["glucoseTargetRangeSchedule"] = glucoseTargetRangeSchedule?.rawValue diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index 9667917786..ad3b7fb42b 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -187,6 +187,13 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas static let count = 3 } + fileprivate enum SettingsRow: Int, CaseCountable { + case retrospectiveCorrection + case integralRetrospectiveCorrection + + static let count = 2 + } + private var eventualGlucoseDescription: String? private var availableInputs: [PredictionInputEffect] = [.carbs, .insulin, .momentum, .retrospection] @@ -204,7 +211,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas case .inputs: return availableInputs.count case .settings: - return 1 + return SettingsRow.count } } @@ -231,12 +238,22 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas case .settings: let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell - cell.titleLabel?.text = NSLocalizedString("Enable Retrospective Correction", comment: "Title of the switch which toggles retrospective correction effects") - 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") - cell.`switch`?.isOn = deviceManager.loopManager.settings.retrospectiveCorrectionEnabled - cell.`switch`?.addTarget(self, action: #selector(retrospectiveCorrectionSwitchChanged(_:)), for: .valueChanged) - - cell.contentView.layoutMargins.left = tableView.separatorInset.left + switch SettingsRow(rawValue: indexPath.row)! { + case .retrospectiveCorrection: + cell.titleLabel?.text = NSLocalizedString("Retrospective Correction", comment: "Title of the switch which toggles retrospective correction effects") + cell.subtitleLabel?.text = NSLocalizedString("More agressively increase or decrease basal delivery when glucose movement over past 30 min doesn't match the carbohydrate and insulin-based model.", comment: "The description of the switch which toggles retrospective correction effects") + cell.`switch`?.isOn = deviceManager.loopManager.settings.retrospectiveCorrectionEnabled + cell.`switch`?.addTarget(self, action: #selector(retrospectiveCorrectionSwitchChanged(_:)), for: .valueChanged) + + cell.contentView.layoutMargins.left = tableView.separatorInset.left + case .integralRetrospectiveCorrection: + cell.titleLabel?.text = NSLocalizedString("Integral Retrospective Correction", comment: "Title of the switch which toggles integral retrospective correction effects") + cell.subtitleLabel?.text = NSLocalizedString("Respond more aggressively to persistent discrepancies in glucose movement.", comment: "The description of the switch which toggles integral retrospective correction effects") + cell.`switch`?.isOn = deviceManager.loopManager.settings.integralRetrospectiveCorrectionEnabled + cell.`switch`?.addTarget(self, action: #selector(integralRetrospectiveCorrectionSwitchChanged(_:)), for: .valueChanged) + + cell.contentView.layoutMargins.left = tableView.separatorInset.left + } return cell } @@ -267,22 +284,48 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) ?? "" - if input == .retrospection, - let startGlucose = retrospectivePredictedGlucose?.first, - let endGlucose = retrospectivePredictedGlucose?.last, - let currentGlucose = self.deviceManager.loopManager.glucoseStore.latestGlucose - { - let formatter = NumberFormatter.glucoseFormatter(for: charts.glucoseUnit) - let values = [startGlucose, endGlucose, currentGlucose].map { formatter.string(from: NSNumber(value: $0.quantity.doubleValue(for: charts.glucoseUnit))) ?? "?" } - let showRetrospectiveCorrectionEffect = formatter.string(from: NSNumber(value: self.deviceManager.loopManager.overallRetrospectiveCorrection.doubleValue(for: charts.glucoseUnit))) ?? "?" - let integralRCIndicator = self.deviceManager.loopManager.integralRectrospectiveCorrectionIndicator - - let retro = String( - format: NSLocalizedString("Last comparison: %1$@ → %2$@ vs %3$@, RC: %4$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)(4: Overall retrospective correction effect)"), - values[0], values[1], values[2], showRetrospectiveCorrectionEffect - ) + integralRCIndicator - - subtitleText = String(format: "%@\n%@", subtitleText, retro) + if input == .retrospection { + if deviceManager.loopManager.settings.retrospectiveCorrectionEnabled, + let startGlucose = retrospectivePredictedGlucose?.first, + let endGlucose = retrospectivePredictedGlucose?.last, + let currentGlucose = self.deviceManager.loopManager.glucoseStore.latestGlucose + { + let formatter = NumberFormatter.glucoseFormatter(for: charts.glucoseUnit) + let values = [startGlucose, endGlucose, currentGlucose].map { formatter.string(from: NSNumber(value: $0.quantity.doubleValue(for: charts.glucoseUnit))) ?? "?" } + let endGlucoseValue = endGlucose.quantity.doubleValue(for: charts.glucoseUnit) + let currentGlucoseValue = currentGlucose.quantity.doubleValue(for: charts.glucoseUnit) + let currentDiscrepancyValue = currentGlucoseValue - endGlucoseValue + let currentDiscrepancy = formatter.string(from: NSNumber(value: currentDiscrepancyValue))! + var integralEffect = "none" + var retrospectiveCorrection = "none" + if self.deviceManager.loopManager.overallRetrospectiveCorrection != nil { + //Retrospective Correction effect included in glucose prediction + let overallRetrospectiveCorrectionValue = self.deviceManager.loopManager.overallRetrospectiveCorrection!.doubleValue(for: charts.glucoseUnit) + let integralEffectValue = overallRetrospectiveCorrectionValue - currentDiscrepancyValue + integralEffect = formatter.string(from: NSNumber(value: integralEffectValue))! + retrospectiveCorrection = formatter.string(from: NSNumber(value: overallRetrospectiveCorrectionValue))! + } + if !deviceManager.loopManager.settings.integralRetrospectiveCorrectionEnabled { + integralEffect = "disabled" + } + let retroComparison = String( + format: NSLocalizedString("Last 30 min comparison: %1$@ → %2$@ vs %3$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)"), + values[0], values[1], values[2]) + let retroCorrection = String( + format: NSLocalizedString("RC effect: %1$@, Integral effect: %2$@\nTotal glucose effect: %3$@", comment: "Format string describing retrospective correction. (1: Current discrepancy)(2: Integral retrospective correction effect)(3: Total retrospective correction effect)"), currentDiscrepancy, integralEffect, retrospectiveCorrection) + + subtitleText = String(format: "%@\n%@", retroComparison, retroCorrection) + + } else { + // Retrospective Correction disabled or not included in glucose prediction for other reasons + if deviceManager.loopManager.settings.retrospectiveCorrectionEnabled { + let inactiveRetrospectiveCorrection = String(format: NSLocalizedString("Temporarily inactive due to recent calibration or missing data", comment: "Format string describing inactive retrospective correction.")) + subtitleText = String(format: "%@", inactiveRetrospectiveCorrection) + } else { + let disabledRetrospectiveCorrection = String(format: NSLocalizedString("⚠️ Retrospective correction is disabled", comment: "Format string describing disabled retrospective correction.")) + subtitleText = String(format: "%@\n%@", subtitleText, disabledRetrospectiveCorrection) + } + } } cell.subtitleLabel?.text = subtitleText @@ -330,11 +373,23 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas @objc private func retrospectiveCorrectionSwitchChanged(_ sender: UISwitch) { deviceManager.loopManager.settings.retrospectiveCorrectionEnabled = sender.isOn - + // if retrospective correction is disabled, integral retrospective correction must also be disabled + if !sender.isOn { + deviceManager.loopManager.settings.integralRetrospectiveCorrectionEnabled = sender.isOn + } + if let row = availableInputs.index(where: { $0 == .retrospection }), let cell = tableView.cellForRow(at: IndexPath(row: row, section: Section.inputs.rawValue)) as? PredictionInputEffectTableViewCell { cell.enabled = self.deviceManager.loopManager.settings.retrospectiveCorrectionEnabled } } + + @objc private func integralRetrospectiveCorrectionSwitchChanged(_ sender: UISwitch) { + deviceManager.loopManager.settings.integralRetrospectiveCorrectionEnabled = sender.isOn + // if integral retrospective correction is enabled, retrospective correction must also be enabled + if sender.isOn { + deviceManager.loopManager.settings.retrospectiveCorrectionEnabled = sender.isOn + } + } } diff --git a/LoopTests/Info.plist b/LoopTests/Info.plist index 6d86f6a299..b8493765dc 100644 --- a/LoopTests/Info.plist +++ b/LoopTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleSignature ???? CFBundleVersion diff --git a/LoopUI/Info.plist b/LoopUI/Info.plist index 6d29dddc6f..a03380ae23 100644 --- a/LoopUI/Info.plist +++ b/LoopUI/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/WatchApp Extension/Info.plist b/WatchApp Extension/Info.plist index b550c08541..3e7d85c8a2 100644 --- a/WatchApp Extension/Info.plist +++ b/WatchApp Extension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleSignature ???? CFBundleVersion diff --git a/WatchApp/Info.plist b/WatchApp/Info.plist index 4123828ce1..cc22fc1ad0 100644 --- a/WatchApp/Info.plist +++ b/WatchApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.5.6 + 1.5.7dev CFBundleSignature ???? CFBundleVersion