From 5a4659c001636e22b66e26e833fc8b25a4fe07b7 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 15 Dec 2022 15:30:39 -0800 Subject: [PATCH 01/25] Use maxBolus and ratio to set maxAutoIOB --- Loop/Managers/DoseMath.swift | 44 ++++++++++++++++++++++++----- Loop/Managers/LoopDataManager.swift | 11 ++++++-- Loop/Models/LoopConstants.swift | 4 +++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 01c9bedfe5..f6fd378a07 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -234,7 +234,9 @@ extension Collection where Element: GlucoseValue { at date: Date, suspendThreshold: HKQuantity, sensitivity: HKQuantity, - model: InsulinModel + model: InsulinModel, + maxAutoIOB: Double?, + insulinOnBoard: Double? ) -> InsulinCorrection? { var minGlucose: GlucoseValue? var eventualGlucose: GlucoseValue? @@ -322,14 +324,32 @@ extension Collection where Element: GlucoseValue { return nil } + // Question: this is the entirelyBelow section of code + // I did not add maxAutoIOB check here as I don't think it is needed + return .entirelyBelowRange( min: min, minTarget: minGlucoseTargets.lowerBound, units: units ) } else if eventual.quantity > eventualGlucoseTargets.upperBound, - let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose + var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose { + // Limit automatic dosing to prevent insulinOnBoard > maxAutoIOB + if minCorrectionUnits > 0 && + insulinOnBoard != nil && + maxAutoIOB != nil && + maxAutoIOB! > 0 { + let checkMaxAutoIOB = maxAutoIOB! - (insulinOnBoard! + minCorrectionUnits) + if checkMaxAutoIOB < 0 { + print("*** maxAutoIOB Feature enacted in aboveRange") + print("*** Initial minCorrectionUnits ", round(100*minCorrectionUnits)/100) + print("*** maxAutoIOB, insulinOnBoard, checkMaxAutoIOB ", round(100*maxAutoIOB!)/100, round(100*insulinOnBoard!)/100, round(100*checkMaxAutoIOB)/100) + minCorrectionUnits = Swift.max(minCorrectionUnits+checkMaxAutoIOB, 0) + print("*** Adjusted minCorrectionUnits ", round(100*minCorrectionUnits)/100) + } + } + return .aboveRange( min: min, correcting: correctingGlucose, @@ -371,14 +391,18 @@ extension Collection where Element: GlucoseValue { rateRounder: ((Double) -> Double)? = nil, isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), - continuationInterval: TimeInterval = .minutes(11) + continuationInterval: TimeInterval = .minutes(11), + insulinOnBoard: Double?, + maxAutoIOB: Double? ) -> TempBasalRecommendation? { let correction = self.insulinCorrection( to: correctionRange, at: date, suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), - model: model + model: model, + maxAutoIOB: maxAutoIOB, + insulinOnBoard: insulinOnBoard ) let scheduledBasalRate = basalRates.value(at: date) @@ -439,14 +463,18 @@ extension Collection where Element: GlucoseValue { rateRounder: ((Double) -> Double)? = nil, isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), - continuationInterval: TimeInterval = .minutes(11) + continuationInterval: TimeInterval = .minutes(11), + insulinOnBoard: Double?, + maxAutoIOB: Double? ) -> AutomaticDoseRecommendation? { guard let correction = self.insulinCorrection( to: correctionRange, at: date, suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), - model: model + model: model, + maxAutoIOB: maxAutoIOB, + insulinOnBoard: insulinOnBoard ) else { return nil } @@ -516,7 +544,9 @@ extension Collection where Element: GlucoseValue { at: date, suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), - model: model + model: model, + maxAutoIOB: nil, + insulinOnBoard: nil ) else { return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin) } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index bc422867fc..dbb9014e85 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1616,6 +1616,9 @@ extension LoopDataManager { let dosingRecommendation: AutomaticDoseRecommendation? + // maxAutoIOB calculated from the user entered maxBolus + let maxAutoIOB = maxBolus! * LoopConstants.ratioMaxAutoInsulinOnBoardToMaxBolus + switch settings.automaticDosingStrategy { case .automaticBolus: let volumeRounder = { (_ units: Double) in @@ -1634,7 +1637,9 @@ extension LoopDataManager { lastTempBasal: lastTempBasal, volumeRounder: volumeRounder, rateRounder: rateRounder, - isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true + isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, + insulinOnBoard: dosingDecision.insulinOnBoard?.value, + maxAutoIOB: maxAutoIOB ) case .tempBasalOnly: let temp = predictedGlucose.recommendedTempBasal( @@ -1647,7 +1652,9 @@ extension LoopDataManager { maxBasalRate: maxBasal!, lastTempBasal: lastTempBasal, rateRounder: rateRounder, - isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true + isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, + insulinOnBoard: dosingDecision.insulinOnBoard?.value, + maxAutoIOB: maxAutoIOB ) dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp) } diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index 67f43e38c0..f56844685a 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -45,6 +45,10 @@ enum LoopConstants { // Percentage of recommended dose to apply as bolus when using automatic bolus dosing strategy static let bolusPartialApplicationFactor = 0.4 + // Simple ratio to limit automatic insulin delivery to <= ratio * maxBolus + // This ratio is multiplied times the user provided Delivery Limit in Therapy Settings + static let ratioMaxAutoInsulinOnBoardToMaxBolus = 1.2 + /// The interval over which to aggregate changes in glucose for retrospective correction static let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) From 2afe19aee3946cec3dec65f8179902644ee74777 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 19 Dec 2022 08:02:46 -0800 Subject: [PATCH 02/25] increase ratioMaxAutoInsulinOnBoardToMaxBolus to 2.0 --- Loop/Models/LoopConstants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index 6a0f197f41..98e288ad6d 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -49,7 +49,7 @@ enum LoopConstants { // Simple ratio to limit automatic insulin delivery to <= ratio * maxBolus // This ratio is multiplied times the user provided Delivery Limit in Therapy Settings - static let ratioMaxAutoInsulinOnBoardToMaxBolus = 1.2 + static let ratioMaxAutoInsulinOnBoardToMaxBolus = 2.0 /// The interval over which to aggregate changes in glucose for retrospective correction static let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) From 0733eafa95e86e3dca3d383f36d899aacec09fbf Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 19 Dec 2022 09:10:11 -0800 Subject: [PATCH 03/25] remove print statements --- Loop/Managers/DoseMath.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index f6fd378a07..bf4935d4ec 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -342,11 +342,8 @@ extension Collection where Element: GlucoseValue { maxAutoIOB! > 0 { let checkMaxAutoIOB = maxAutoIOB! - (insulinOnBoard! + minCorrectionUnits) if checkMaxAutoIOB < 0 { - print("*** maxAutoIOB Feature enacted in aboveRange") - print("*** Initial minCorrectionUnits ", round(100*minCorrectionUnits)/100) - print("*** maxAutoIOB, insulinOnBoard, checkMaxAutoIOB ", round(100*maxAutoIOB!)/100, round(100*insulinOnBoard!)/100, round(100*checkMaxAutoIOB)/100) + // TO DO - nice to have logging but not required minCorrectionUnits = Swift.max(minCorrectionUnits+checkMaxAutoIOB, 0) - print("*** Adjusted minCorrectionUnits ", round(100*minCorrectionUnits)/100) } } From 90b0470f548ec803e23885856450dbc962900690 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 19 Dec 2022 10:49:01 -0800 Subject: [PATCH 04/25] restore LoopContants --- Loop/Models/LoopConstants.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index 98e288ad6d..1f439c08ef 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -47,10 +47,6 @@ enum LoopConstants { // Percentage of recommended dose to apply as bolus when using automatic bolus dosing strategy static let bolusPartialApplicationFactor = 0.4 - // Simple ratio to limit automatic insulin delivery to <= ratio * maxBolus - // This ratio is multiplied times the user provided Delivery Limit in Therapy Settings - static let ratioMaxAutoInsulinOnBoardToMaxBolus = 2.0 - /// The interval over which to aggregate changes in glucose for retrospective correction static let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) From 74fd253c77a0db4968b976ab93da08d3872aa37c Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 19 Dec 2022 10:52:54 -0800 Subject: [PATCH 05/25] modify name from maxAutoIOB to automaticDosingIOBLimit --- Loop/Managers/DoseMath.swift | 27 ++++++++++++--------------- Loop/Managers/LoopDataManager.swift | 8 ++++---- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index bf4935d4ec..b183f84bae 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -235,7 +235,7 @@ extension Collection where Element: GlucoseValue { suspendThreshold: HKQuantity, sensitivity: HKQuantity, model: InsulinModel, - maxAutoIOB: Double?, + automaticDosingIOBLimit: Double?, insulinOnBoard: Double? ) -> InsulinCorrection? { var minGlucose: GlucoseValue? @@ -324,9 +324,6 @@ extension Collection where Element: GlucoseValue { return nil } - // Question: this is the entirelyBelow section of code - // I did not add maxAutoIOB check here as I don't think it is needed - return .entirelyBelowRange( min: min, minTarget: minGlucoseTargets.lowerBound, @@ -335,15 +332,15 @@ extension Collection where Element: GlucoseValue { } else if eventual.quantity > eventualGlucoseTargets.upperBound, var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose { - // Limit automatic dosing to prevent insulinOnBoard > maxAutoIOB + // Limit automatic dosing to prevent insulinOnBoard > automaticDosingIOBLimit if minCorrectionUnits > 0 && insulinOnBoard != nil && - maxAutoIOB != nil && - maxAutoIOB! > 0 { - let checkMaxAutoIOB = maxAutoIOB! - (insulinOnBoard! + minCorrectionUnits) - if checkMaxAutoIOB < 0 { + automaticDosingIOBLimit != nil && + automaticDosingIOBLimit! > 0 { + let checkAutomaticDosing = automaticDosingIOBLimit! - (insulinOnBoard! + minCorrectionUnits) + if checkAutomaticDosing < 0 { // TO DO - nice to have logging but not required - minCorrectionUnits = Swift.max(minCorrectionUnits+checkMaxAutoIOB, 0) + minCorrectionUnits = Swift.max(minCorrectionUnits+checkAutomaticDosing, 0) } } @@ -390,7 +387,7 @@ extension Collection where Element: GlucoseValue { duration: TimeInterval = .minutes(30), continuationInterval: TimeInterval = .minutes(11), insulinOnBoard: Double?, - maxAutoIOB: Double? + automaticDosingIOBLimit: Double? ) -> TempBasalRecommendation? { let correction = self.insulinCorrection( to: correctionRange, @@ -398,7 +395,7 @@ extension Collection where Element: GlucoseValue { suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), model: model, - maxAutoIOB: maxAutoIOB, + automaticDosingIOBLimit: automaticDosingIOBLimit, insulinOnBoard: insulinOnBoard ) @@ -462,7 +459,7 @@ extension Collection where Element: GlucoseValue { duration: TimeInterval = .minutes(30), continuationInterval: TimeInterval = .minutes(11), insulinOnBoard: Double?, - maxAutoIOB: Double? + automaticDosingIOBLimit: Double? ) -> AutomaticDoseRecommendation? { guard let correction = self.insulinCorrection( to: correctionRange, @@ -470,7 +467,7 @@ extension Collection where Element: GlucoseValue { suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), model: model, - maxAutoIOB: maxAutoIOB, + automaticDosingIOBLimit: automaticDosingIOBLimit, insulinOnBoard: insulinOnBoard ) else { return nil @@ -542,7 +539,7 @@ extension Collection where Element: GlucoseValue { suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), model: model, - maxAutoIOB: nil, + automaticDosingIOBLimit: nil, insulinOnBoard: nil ) else { return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index dbb9014e85..be85203c3a 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1616,8 +1616,8 @@ extension LoopDataManager { let dosingRecommendation: AutomaticDoseRecommendation? - // maxAutoIOB calculated from the user entered maxBolus - let maxAutoIOB = maxBolus! * LoopConstants.ratioMaxAutoInsulinOnBoardToMaxBolus + // automaticDosingIOBLimit calculated from the user entered maxBolus + let automaticDosingIOBLimit = maxBolus! * 2.0 switch settings.automaticDosingStrategy { case .automaticBolus: @@ -1639,7 +1639,7 @@ extension LoopDataManager { rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, insulinOnBoard: dosingDecision.insulinOnBoard?.value, - maxAutoIOB: maxAutoIOB + automaticDosingIOBLimit: automaticDosingIOBLimit ) case .tempBasalOnly: let temp = predictedGlucose.recommendedTempBasal( @@ -1654,7 +1654,7 @@ extension LoopDataManager { rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, insulinOnBoard: dosingDecision.insulinOnBoard?.value, - maxAutoIOB: maxAutoIOB + automaticDosingIOBLimit: automaticDosingIOBLimit ) dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp) } From 34bf4cb2dca9734a2c862a00dc9ee409fce92f8c Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 29 Dec 2022 13:53:58 -0800 Subject: [PATCH 06/25] Code cleanup in DoseMath --- Loop/Managers/DoseMath.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index b183f84bae..d38aa345f3 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -333,11 +333,8 @@ extension Collection where Element: GlucoseValue { var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose { // Limit automatic dosing to prevent insulinOnBoard > automaticDosingIOBLimit - if minCorrectionUnits > 0 && - insulinOnBoard != nil && - automaticDosingIOBLimit != nil && - automaticDosingIOBLimit! > 0 { - let checkAutomaticDosing = automaticDosingIOBLimit! - (insulinOnBoard! + minCorrectionUnits) + if let automaticDosingIOBLimit = automaticDosingIOBLimit, let insulinOnBoard = insulinOnBoard, minCorrectionUnits > 0 { + let checkAutomaticDosing = automaticDosingIOBLimit - (insulinOnBoard + minCorrectionUnits) if checkAutomaticDosing < 0 { // TO DO - nice to have logging but not required minCorrectionUnits = Swift.max(minCorrectionUnits+checkAutomaticDosing, 0) From d6698c946248108e5e02317f3a7a4a2feb6f49b1 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 29 Dec 2022 18:19:44 -0800 Subject: [PATCH 07/25] configure new optional commands with default nil DoseMathTests should work without modification --- Loop/Managers/DoseMath.swift | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index d38aa345f3..4d0b2ea146 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -235,8 +235,8 @@ extension Collection where Element: GlucoseValue { suspendThreshold: HKQuantity, sensitivity: HKQuantity, model: InsulinModel, - automaticDosingIOBLimit: Double?, - insulinOnBoard: Double? + insulinOnBoard: Double? = nil, + automaticDosingIOBLimit: Double? = nil ) -> InsulinCorrection? { var minGlucose: GlucoseValue? var eventualGlucose: GlucoseValue? @@ -333,7 +333,10 @@ extension Collection where Element: GlucoseValue { var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose { // Limit automatic dosing to prevent insulinOnBoard > automaticDosingIOBLimit - if let automaticDosingIOBLimit = automaticDosingIOBLimit, let insulinOnBoard = insulinOnBoard, minCorrectionUnits > 0 { + if let automaticDosingIOBLimit = automaticDosingIOBLimit, + let insulinOnBoard = insulinOnBoard, + minCorrectionUnits > 0 + { let checkAutomaticDosing = automaticDosingIOBLimit - (insulinOnBoard + minCorrectionUnits) if checkAutomaticDosing < 0 { // TO DO - nice to have logging but not required @@ -383,8 +386,8 @@ extension Collection where Element: GlucoseValue { isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), continuationInterval: TimeInterval = .minutes(11), - insulinOnBoard: Double?, - automaticDosingIOBLimit: Double? + insulinOnBoard: Double? = nil, + automaticDosingIOBLimit: Double? = nil ) -> TempBasalRecommendation? { let correction = self.insulinCorrection( to: correctionRange, @@ -392,8 +395,8 @@ extension Collection where Element: GlucoseValue { suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), model: model, - automaticDosingIOBLimit: automaticDosingIOBLimit, - insulinOnBoard: insulinOnBoard + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) let scheduledBasalRate = basalRates.value(at: date) @@ -455,8 +458,8 @@ extension Collection where Element: GlucoseValue { isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), continuationInterval: TimeInterval = .minutes(11), - insulinOnBoard: Double?, - automaticDosingIOBLimit: Double? + insulinOnBoard: Double? = nil, + automaticDosingIOBLimit: Double? = nil ) -> AutomaticDoseRecommendation? { guard let correction = self.insulinCorrection( to: correctionRange, @@ -464,8 +467,8 @@ extension Collection where Element: GlucoseValue { suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), model: model, - automaticDosingIOBLimit: automaticDosingIOBLimit, - insulinOnBoard: insulinOnBoard + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) else { return nil } @@ -536,8 +539,8 @@ extension Collection where Element: GlucoseValue { suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), model: model, - automaticDosingIOBLimit: nil, - insulinOnBoard: nil + insulinOnBoard: nil, + automaticDosingIOBLimit: nil ) else { return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin) } From 0c182a10e18900475d9c1b24ca728ee2f32efb87 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 29 Dec 2022 18:34:44 -0800 Subject: [PATCH 08/25] remove whitespace --- Loop/Managers/LoopDataManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index be85203c3a..feb69a28a7 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1618,7 +1618,7 @@ extension LoopDataManager { // automaticDosingIOBLimit calculated from the user entered maxBolus let automaticDosingIOBLimit = maxBolus! * 2.0 - + switch settings.automaticDosingStrategy { case .automaticBolus: let volumeRounder = { (_ units: Double) in From fc654ff05812379ecff4e3d9890febbfed0f9132 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 30 Dec 2022 03:50:41 -0800 Subject: [PATCH 09/25] Add automaticIOBLimitTests --- DoseMathTests/DoseMathTests.swift | 229 ++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index ce9e1767bf..2bb5b1203c 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -1504,3 +1504,232 @@ class RecommendBolusTests: XCTestCase { XCTAssertEqual(1.96, dose.amount, accuracy: 1.0 / 40.0) } } + +class automaticIOBLimitTests: XCTestCase { + + fileprivate let maxBasalRate = 3.0 + + fileprivate let maxBolus = 5.0 + + fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } + + func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseFixtureValue] { + let fixture: [JSONDictionary] = loadFixture(resourceName) + let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter() + + return fixture.map { + return GlucoseFixtureValue( + startDate: dateFormatter.date(from: $0["date"] as! String)!, + quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double) + ) + } + } + + func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { + let fixture: [JSONDictionary] = loadFixture(resourceName) + + let items = fixture.map { + return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) + } + + return BasalRateSchedule(dailyItems: items)! + } + + var basalRateSchedule: BasalRateSchedule { + return loadBasalRateScheduleFixture("read_selected_basal_profile") + } + + var glucoseTargetRange: GlucoseRangeSchedule { + return GlucoseRangeSchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 90, maxValue: 120))])! + } + + var insulinSensitivitySchedule: InsulinSensitivitySchedule { + return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])! + } + + var suspendThreshold: GlucoseThreshold { + return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 55) + } + + var exponentialInsulinModel: InsulinModel = ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0, delay: 0) + + var automaticDosingIOBLimit: Double { + return 2.0 * maxBolus + } + + var insulinOnBoard: Double = 0.0 + + // First series of tests return bolus of 2.30 U (no limits) + // adjust IOB to modify result + + func testFlatAndHighAutomaticBolusNoLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: 1.0, + lastTempBasal: nil, + insulinOnBoard: 0, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(2.30, dose!.bolusUnits!, accuracy: 1.0 / 40.0) + } + + func testFlatAndHighAutomaticBolusWithLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + // max allowed dose will be 0.5 units + let maxAutoCorrection = 0.5 + insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: 1.0, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) + } + + func testFlatAndHighAutomaticBolusWithLimitPAF() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + // max allowed dose will be 0.5 units + // for this test use partialApplicationFactor of 0.5, instead of 1.0 + let maxAutoCorrection = 0.5 + let partialApplicationFactor = 0.5 + insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: partialApplicationFactor, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(partialApplicationFactor * maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) + } + + func testFlatAndHighAutomaticBolusFullLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + // no automatic dose + insulinOnBoard = automaticDosingIOBLimit + 0.1 + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: 1.0, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose) + } + + // Next set of test are for TempBasal, 3.0 U/hr no limit + // Adjust IOB to modify + let noLimitBasalRate = 3.0 + let scheduledBasalRate = 0.8 + + func testHighAndRisingTempBasalNoLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + + // no limit + insulinOnBoard = 0.0 + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: self.insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertEqual(noLimitBasalRate, dose!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) + } + + func testHighAndRisingTempBasalWithLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + + // max allowed dose will be 0.5 units, temp basal twice that + let maxAutoCorrection = 0.5 + let limitedBasalRate = scheduledBasalRate + 2.0 * maxAutoCorrection + insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: self.insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertEqual(limitedBasalRate, dose!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) + } + + func testHighAndRisingTempBasalFullLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + + // no automatic dose + insulinOnBoard = automaticDosingIOBLimit + 0.1 + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: self.insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose) + } +} From 87478d8728d2f6433c885c419cfb6168673b680e Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 30 Dec 2022 21:23:31 -0800 Subject: [PATCH 10/25] DoseMathTests: add new args to all automated dosing tests --- DoseMathTests/DoseMathTests.swift | 719 ++++++++++++++++-------------- 1 file changed, 379 insertions(+), 340 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 2bb5b1203c..d11878e04b 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -55,6 +55,8 @@ class RecommendTempBasalTests: XCTestCase { fileprivate let maxBasalRate = 3.0 + fileprivate let maxBolus = 5.0 + fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseFixtureValue] { @@ -105,6 +107,12 @@ class RecommendTempBasalTests: XCTestCase { return TimeInterval(hours: 4) } + var automaticDosingIOBLimit: Double { + return 2.0 * maxBolus + } + + var insulinOnBoard: Double = 0.0 + func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") @@ -116,7 +124,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -133,7 +143,9 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -151,7 +163,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -169,7 +183,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true + isBasalRateScheduleOverrideActive: true, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0.8, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -189,7 +205,9 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true + isBasalRateScheduleOverrideActive: true, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -208,7 +226,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -230,7 +250,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -249,7 +271,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -272,7 +296,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -293,7 +319,9 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true + isBasalRateScheduleOverrideActive: true, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -318,7 +346,9 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: lastTempBasal, - isBasalRateScheduleOverrideActive: true + isBasalRateScheduleOverrideActive: true, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -335,7 +365,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -356,7 +388,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -374,7 +408,9 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -395,7 +431,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -414,7 +452,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -436,7 +476,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -464,7 +506,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -478,7 +522,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -505,7 +551,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -521,7 +569,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -538,7 +588,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -557,7 +609,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -576,7 +630,9 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -594,7 +650,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -616,7 +674,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -635,7 +695,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -657,7 +719,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -676,7 +740,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(3.0, dose!.unitsPerHour) @@ -695,7 +761,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose!.basalAdjustment) @@ -715,7 +783,9 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true + isBasalRateScheduleOverrideActive: true, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -741,7 +811,9 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: lastTempBasal, - isBasalRateScheduleOverrideActive: true + isBasalRateScheduleOverrideActive: true, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose!.basalAdjustment) @@ -762,7 +834,9 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(1.425, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -781,7 +855,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose!.basalAdjustment) @@ -799,7 +875,9 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(2.68, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -818,7 +896,9 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose!.basalAdjustment) @@ -836,7 +916,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(1.60, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -854,7 +936,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(3.0, dose!.unitsPerHour) @@ -871,7 +955,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(2.975, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -889,7 +975,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertEqual(0.0, dose!.unitsPerHour) @@ -907,7 +995,9 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -924,7 +1014,9 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) @@ -941,104 +1033,280 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) XCTAssertNil(dose) } -} - - -class RecommendBolusTests: XCTestCase { - - fileprivate let maxBolus = 10.0 - - fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } - - func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseFixtureValue] { - let fixture: [JSONDictionary] = loadFixture(resourceName) - let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter() - - return fixture.map { - return GlucoseFixtureValue( - startDate: dateFormatter.date(from: $0["date"] as! String)!, - quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double) - ) - } - } - - func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { - let fixture: [JSONDictionary] = loadFixture(resourceName) - - let items = fixture.map { - return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) - } - - return BasalRateSchedule(dailyItems: items)! - } - var basalRateSchedule: BasalRateSchedule { - return loadBasalRateScheduleFixture("read_selected_basal_profile") - } + // Next four tests, recommended bolus is 2.30 U (no limit) + // adjust IOB to modify AutomaticBolus - var glucoseTargetRange: GlucoseRangeSchedule { - return GlucoseRangeSchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 90, maxValue: 120))])! - } + func testFlatAndHighAutomaticBolusNoLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - var insulinSensitivitySchedule: InsulinSensitivitySchedule { - return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])! - } - - var suspendThreshold: GlucoseThreshold { - return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 55) - } - - var exponentialInsulinModel: InsulinModel = ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0, delay: 0) + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: 1.0, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) - var walshInsulinModel: InsulinModel { - return WalshInsulinModel(actionDuration: insulinActionDuration) + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(2.30, dose!.bolusUnits!, accuracy: 1.0 / 40.0) } - var insulinActionDuration: TimeInterval { - return TimeInterval(hours: 4) - } + func testFlatAndHighAutomaticBolusWithLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - func testNoChange() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + // max allowed dose will be 0.5 units + let maxAutoCorrection = 0.5 + insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - let dose = glucose.recommendedManualBolus( + let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, sensitivity: insulinSensitivitySchedule, - model: walshInsulinModel, - pendingInsulin: 0, - maxBolus: maxBolus + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: 1.0, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) - XCTAssertEqual(0, dose.amount) + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) } - func testStartHighEndInRange() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + func testFlatAndHighAutomaticBolusWithLimitPAF() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - let dose = glucose.recommendedManualBolus( + // max allowed dose will be 0.5 units + // for this test use partialApplicationFactor of 0.5, instead of 1.0 + let maxAutoCorrection = 0.5 + let partialApplicationFactor = 0.5 + insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + + let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, sensitivity: insulinSensitivitySchedule, - model: walshInsulinModel, - pendingInsulin: 0, - maxBolus: maxBolus + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: partialApplicationFactor, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit ) - XCTAssertEqual(0, dose.amount) + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(partialApplicationFactor * maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) } - - func testStartHighEndInRangeExponentialModel() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") - let dose = glucose.recommendedManualBolus( + func testFlatAndHighAutomaticBolusFullLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + // no automatic dose + insulinOnBoard = automaticDosingIOBLimit + 0.1 + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: maxBolus, + partialApplicationFactor: 1.0, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose) + } + + // Next three test recommended TempBasal is 3.0 U/hr (no limit) + // Adjust IOB to modify automatic TempBasal + let noLimitBasalRate = 3.0 + let scheduledBasalRate = 0.8 + + func testHighAndRisingTempBasalNoLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + + // no limit + insulinOnBoard = 0.0 + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: self.insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertEqual(noLimitBasalRate, dose!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) + } + + func testHighAndRisingTempBasalWithLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + + // max allowed dose will be 0.5 units, temp basal twice that + let maxAutoCorrection = 0.5 + let limitedBasalRate = scheduledBasalRate + 2.0 * maxAutoCorrection + insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: self.insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertEqual(limitedBasalRate, dose!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) + } + + func testHighAndRisingTempBasalFullLimit() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + + // no automatic dose + insulinOnBoard = automaticDosingIOBLimit + 0.1 + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: self.insulinSensitivitySchedule, + model: exponentialInsulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + lastTempBasal: nil, + insulinOnBoard: insulinOnBoard, + automaticDosingIOBLimit: automaticDosingIOBLimit + ) + + XCTAssertNil(dose) + } +} + + +class RecommendBolusTests: XCTestCase { + + fileprivate let maxBolus = 10.0 + + fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } + + func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseFixtureValue] { + let fixture: [JSONDictionary] = loadFixture(resourceName) + let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter() + + return fixture.map { + return GlucoseFixtureValue( + startDate: dateFormatter.date(from: $0["date"] as! String)!, + quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double) + ) + } + } + + func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { + let fixture: [JSONDictionary] = loadFixture(resourceName) + + let items = fixture.map { + return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) + } + + return BasalRateSchedule(dailyItems: items)! + } + + var basalRateSchedule: BasalRateSchedule { + return loadBasalRateScheduleFixture("read_selected_basal_profile") + } + + var glucoseTargetRange: GlucoseRangeSchedule { + return GlucoseRangeSchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 90, maxValue: 120))])! + } + + var insulinSensitivitySchedule: InsulinSensitivitySchedule { + return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])! + } + + var suspendThreshold: GlucoseThreshold { + return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 55) + } + + var exponentialInsulinModel: InsulinModel = ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0, delay: 0) + + var walshInsulinModel: InsulinModel { + return WalshInsulinModel(actionDuration: insulinActionDuration) + } + + var insulinActionDuration: TimeInterval { + return TimeInterval(hours: 4) + } + + func testNoChange() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + + let dose = glucose.recommendedManualBolus( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: walshInsulinModel, + pendingInsulin: 0, + maxBolus: maxBolus + ) + + XCTAssertEqual(0, dose.amount) + } + + func testStartHighEndInRange() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + + let dose = glucose.recommendedManualBolus( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: walshInsulinModel, + pendingInsulin: 0, + maxBolus: maxBolus + ) + + XCTAssertEqual(0, dose.amount) + } + + func testStartHighEndInRangeExponentialModel() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -1504,232 +1772,3 @@ class RecommendBolusTests: XCTestCase { XCTAssertEqual(1.96, dose.amount, accuracy: 1.0 / 40.0) } } - -class automaticIOBLimitTests: XCTestCase { - - fileprivate let maxBasalRate = 3.0 - - fileprivate let maxBolus = 5.0 - - fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } - - func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseFixtureValue] { - let fixture: [JSONDictionary] = loadFixture(resourceName) - let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter() - - return fixture.map { - return GlucoseFixtureValue( - startDate: dateFormatter.date(from: $0["date"] as! String)!, - quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double) - ) - } - } - - func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { - let fixture: [JSONDictionary] = loadFixture(resourceName) - - let items = fixture.map { - return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) - } - - return BasalRateSchedule(dailyItems: items)! - } - - var basalRateSchedule: BasalRateSchedule { - return loadBasalRateScheduleFixture("read_selected_basal_profile") - } - - var glucoseTargetRange: GlucoseRangeSchedule { - return GlucoseRangeSchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 90, maxValue: 120))])! - } - - var insulinSensitivitySchedule: InsulinSensitivitySchedule { - return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])! - } - - var suspendThreshold: GlucoseThreshold { - return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 55) - } - - var exponentialInsulinModel: InsulinModel = ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0, delay: 0) - - var automaticDosingIOBLimit: Double { - return 2.0 * maxBolus - } - - var insulinOnBoard: Double = 0.0 - - // First series of tests return bolus of 2.30 U (no limits) - // adjust IOB to modify result - - func testFlatAndHighAutomaticBolusNoLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: 1.0, - lastTempBasal: nil, - insulinOnBoard: 0, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose!.basalAdjustment) - XCTAssertEqual(2.30, dose!.bolusUnits!, accuracy: 1.0 / 40.0) - } - - func testFlatAndHighAutomaticBolusWithLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - // max allowed dose will be 0.5 units - let maxAutoCorrection = 0.5 - insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: 1.0, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose!.basalAdjustment) - XCTAssertEqual(maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) - } - - func testFlatAndHighAutomaticBolusWithLimitPAF() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - // max allowed dose will be 0.5 units - // for this test use partialApplicationFactor of 0.5, instead of 1.0 - let maxAutoCorrection = 0.5 - let partialApplicationFactor = 0.5 - insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: partialApplicationFactor, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose!.basalAdjustment) - XCTAssertEqual(partialApplicationFactor * maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) - } - - func testFlatAndHighAutomaticBolusFullLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - // no automatic dose - insulinOnBoard = automaticDosingIOBLimit + 0.1 - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: 1.0, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose) - } - - // Next set of test are for TempBasal, 3.0 U/hr no limit - // Adjust IOB to modify - let noLimitBasalRate = 3.0 - let scheduledBasalRate = 0.8 - - func testHighAndRisingTempBasalNoLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - - // no limit - insulinOnBoard = 0.0 - - let dose = glucose.recommendedTempBasal( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: self.insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertEqual(noLimitBasalRate, dose!.unitsPerHour) - XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) - } - - func testHighAndRisingTempBasalWithLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - - // max allowed dose will be 0.5 units, temp basal twice that - let maxAutoCorrection = 0.5 - let limitedBasalRate = scheduledBasalRate + 2.0 * maxAutoCorrection - insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - - let dose = glucose.recommendedTempBasal( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: self.insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertEqual(limitedBasalRate, dose!.unitsPerHour) - XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) - } - - func testHighAndRisingTempBasalFullLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - - // no automatic dose - insulinOnBoard = automaticDosingIOBLimit + 0.1 - - let dose = glucose.recommendedTempBasal( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: self.insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose) - } -} From fae59f40c355bc2466118235972cf84c15c1aa91 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 30 Dec 2022 21:44:26 -0800 Subject: [PATCH 11/25] remove defaults so new parameters are required --- Loop/Managers/DoseMath.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 4d0b2ea146..88e3c4fcca 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -235,8 +235,8 @@ extension Collection where Element: GlucoseValue { suspendThreshold: HKQuantity, sensitivity: HKQuantity, model: InsulinModel, - insulinOnBoard: Double? = nil, - automaticDosingIOBLimit: Double? = nil + insulinOnBoard: Double?, + automaticDosingIOBLimit: Double? ) -> InsulinCorrection? { var minGlucose: GlucoseValue? var eventualGlucose: GlucoseValue? @@ -386,8 +386,8 @@ extension Collection where Element: GlucoseValue { isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), continuationInterval: TimeInterval = .minutes(11), - insulinOnBoard: Double? = nil, - automaticDosingIOBLimit: Double? = nil + insulinOnBoard: Double?, + automaticDosingIOBLimit: Double? ) -> TempBasalRecommendation? { let correction = self.insulinCorrection( to: correctionRange, @@ -458,8 +458,8 @@ extension Collection where Element: GlucoseValue { isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), continuationInterval: TimeInterval = .minutes(11), - insulinOnBoard: Double? = nil, - automaticDosingIOBLimit: Double? = nil + insulinOnBoard: Double?, + automaticDosingIOBLimit: Double? ) -> AutomaticDoseRecommendation? { guard let correction = self.insulinCorrection( to: correctionRange, From ea948df1a13ce200a63c99122f2a0a11bfe9589f Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sun, 1 Jan 2023 16:59:59 -0800 Subject: [PATCH 12/25] Modify method for providing insulinOnBoard in LoopDataManager --- Loop/Managers/LoopDataManager.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 7fd470ecc9..c7332b7993 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -63,6 +63,8 @@ final class LoopDataManager { private var timeBasedDoseApplicationFactor: Double = 1.0 + private var insulinOnBoardValue: Double? + deinit { for observer in notificationObservers { NotificationCenter.default.removeObserver(observer) @@ -1044,6 +1046,7 @@ extension LoopDataManager { warnings.append(.fetchDataWarning(.insulinOnBoard(error: error))) case .success(let insulinValue): insulinOnBoard = insulinValue + self.insulinOnBoardValue = insulinValue.value } updateGroup.leave() } @@ -1651,7 +1654,7 @@ extension LoopDataManager { volumeRounder: volumeRounder, rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, - insulinOnBoard: dosingDecision.insulinOnBoard?.value, + insulinOnBoard: self.insulinOnBoardValue, automaticDosingIOBLimit: automaticDosingIOBLimit ) case .tempBasalOnly: @@ -1666,7 +1669,7 @@ extension LoopDataManager { lastTempBasal: lastTempBasal, rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, - insulinOnBoard: dosingDecision.insulinOnBoard?.value, + insulinOnBoard: self.insulinOnBoardValue, automaticDosingIOBLimit: automaticDosingIOBLimit ) dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp) From 800eba0a71e34528350cf66ad6716b108b3f6a51 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sun, 1 Jan 2023 21:28:56 -0800 Subject: [PATCH 13/25] AlertManagerTests: add new parameter --- .../Managers/Alerts/AlertManagerTests.swift | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/LoopTests/Managers/Alerts/AlertManagerTests.swift b/LoopTests/Managers/Alerts/AlertManagerTests.swift index 0347645323..60ae0837ff 100644 --- a/LoopTests/Managers/Alerts/AlertManagerTests.swift +++ b/LoopTests/Managers/Alerts/AlertManagerTests.swift @@ -172,6 +172,8 @@ class AlertManagerTests: XCTestCase { var alertManager: AlertManager! var isInBackground = true + private var analyticsServicesManager = AnalyticsServicesManager() + override func setUp() { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UNUserNotificationCenter.current().removeAllDeliveredNotifications() @@ -186,6 +188,7 @@ class AlertManagerTests: XCTestCase { fileManager: mockFileManager, alertStore: mockAlertStore, bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager, preventIssuanceBeforePlayback: false) } @@ -263,7 +266,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) alertManager.playbackAlertsFromPersistence() XCTAssertEqual(alert, mockModalScheduler.scheduledAlert) XCTAssertNil(mockUserNotificationScheduler.scheduledAlert) @@ -284,7 +288,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) alertManager.playbackAlertsFromPersistence() let expected = Alert(identifier: Self.mockIdentifier, foregroundContent: content, backgroundContent: content, trigger: .immediate) XCTAssertEqual(expected, mockModalScheduler.scheduledAlert) @@ -306,7 +311,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) alertManager.playbackAlertsFromPersistence() // The trigger for this should be `.delayed` by "something less than 15 seconds", @@ -336,7 +342,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) alertManager.playbackAlertsFromPersistence() XCTAssertEqual(alert, mockModalScheduler.scheduledAlert) @@ -358,7 +365,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) alertManager.lookupAllUnretracted(managerIdentifier: Self.mockManagerIdentifier) { result in try? XCTAssertEqual([PersistedAlert(alert: alert, issuedDate: date, retractedDate: nil, acknowledgedDate: nil)], XCTUnwrap(result.successValue)) @@ -380,7 +388,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) alertManager.lookupAllUnacknowledgedUnretracted(managerIdentifier: Self.mockManagerIdentifier) { result in try? XCTAssertEqual([PersistedAlert(alert: alert, issuedDate: date, retractedDate: nil, acknowledgedDate: nil)], XCTUnwrap(result.successValue)) @@ -402,7 +411,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) let identifierExists = Self.mockIdentifier let identifierDoesNotExist = Alert.Identifier(managerIdentifier: "TestManagerIdentifier", alertIdentifier: "TestAlertIdentifier") alertManager.doesIssuedAlertExist(identifier: identifierExists) { result in @@ -425,7 +435,8 @@ class AlertManagerTests: XCTestCase { userNotificationAlertScheduler: mockUserNotificationScheduler, fileManager: mockFileManager, alertStore: mockAlertStore, - bluetoothProvider: MockBluetoothProvider()) + bluetoothProvider: MockBluetoothProvider(), + analyticsServicesManager: analyticsServicesManager) let now = Date() alertManager.recordRetractedAlert(alert, at: now) XCTAssertEqual(mockAlertStore.retractedAlert, alert) From 0ef50083eabfb873c0aeb826399597f8432dd4db Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 2 Jan 2023 11:25:32 -0800 Subject: [PATCH 14/25] match whitespace --- LoopTests/Managers/Alerts/AlertManagerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LoopTests/Managers/Alerts/AlertManagerTests.swift b/LoopTests/Managers/Alerts/AlertManagerTests.swift index ad66b23d73..18026c1e82 100644 --- a/LoopTests/Managers/Alerts/AlertManagerTests.swift +++ b/LoopTests/Managers/Alerts/AlertManagerTests.swift @@ -171,7 +171,7 @@ class AlertManagerTests: XCTestCase { var mockAlertStore: MockAlertStore! var alertManager: AlertManager! var isInBackground = true - + override func setUp() { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UNUserNotificationCenter.current().removeAllDeliveredNotifications() From c902fbe048aaa570fccaa8fb13479080c1e2974c Mon Sep 17 00:00:00 2001 From: Anna Quinlan Date: Tue, 3 Jan 2023 10:13:48 -0800 Subject: [PATCH 15/25] `insulinOnBoardValue` -> `insulinOnBoard` for logging purposes --- Loop/Managers/LoopDataManager.swift | 23 +++++++++++-------- .../ViewModels/BolusEntryViewModelTests.swift | 2 ++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index c7332b7993..8ff5de86bb 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -63,7 +63,7 @@ final class LoopDataManager { private var timeBasedDoseApplicationFactor: Double = 1.0 - private var insulinOnBoardValue: Double? + private var insulinOnBoard: InsulinValue? deinit { for observer in notificationObservers { @@ -1036,17 +1036,13 @@ extension LoopDataManager { updateGroup.leave() } } - - var insulinOnBoard: InsulinValue? - updateGroup.enter() doseStore.insulinOnBoard(at: now()) { result in switch result { case .failure(let error): warnings.append(.fetchDataWarning(.insulinOnBoard(error: error))) case .success(let insulinValue): - insulinOnBoard = insulinValue - self.insulinOnBoardValue = insulinValue.value + self.insulinOnBoard = insulinValue } updateGroup.leave() } @@ -1067,7 +1063,7 @@ extension LoopDataManager { dosingDecision.date = now() dosingDecision.historicalGlucose = historicalGlucose dosingDecision.carbsOnBoard = carbsOnBoard - dosingDecision.insulinOnBoard = insulinOnBoard + dosingDecision.insulinOnBoard = self.insulinOnBoard dosingDecision.glucoseTargetRangeSchedule = settings.effectiveGlucoseTargetRangeSchedule() // These will be updated by updatePredictedGlucoseAndRecommendedDose, if possible @@ -1654,7 +1650,7 @@ extension LoopDataManager { volumeRounder: volumeRounder, rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, - insulinOnBoard: self.insulinOnBoardValue, + insulinOnBoard: self.insulinOnBoard?.value, automaticDosingIOBLimit: automaticDosingIOBLimit ) case .tempBasalOnly: @@ -1669,7 +1665,7 @@ extension LoopDataManager { lastTempBasal: lastTempBasal, rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, - insulinOnBoard: self.insulinOnBoardValue, + insulinOnBoard: self.insulinOnBoard?.value, automaticDosingIOBLimit: automaticDosingIOBLimit ) dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp) @@ -1755,6 +1751,9 @@ extension LoopDataManager { protocol LoopState { /// The last-calculated carbs on board var carbsOnBoard: CarbValue? { get } + + /// The last-calculated insulin on board + var insulinOnBoard: InsulinValue? { get } /// An error in the current state of the loop, or one that happened during the last attempt to loop. var error: LoopError? { get } @@ -1857,6 +1856,11 @@ extension LoopDataManager { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) return loopDataManager.carbsOnBoard } + + var insulinOnBoard: InsulinValue? { + dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) + return loopDataManager.insulinOnBoard + } var error: LoopError? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) @@ -2061,6 +2065,7 @@ extension LoopDataManager { "lastLoopCompleted: \(String(describing: manager.lastLoopCompleted))", "basalDeliveryState: \(String(describing: manager.basalDeliveryState))", "carbsOnBoard: \(String(describing: state.carbsOnBoard))", + "insulinOnBoard: \(String(describing: manager.insulinOnBoard))", "error: \(String(describing: state.error))", "overrideInUserDefaults: \(String(describing: UserDefaults.appGroup?.intentExtensionOverrideToSet))", "", diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 7e2a66c1df..01c577b4a5 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -764,6 +764,8 @@ fileprivate class MockLoopState: LoopState { var carbsOnBoard: CarbValue? + var insulinOnBoard: InsulinValue? + var error: LoopError? var insulinCounteractionEffects: [GlucoseEffectVelocity] = [] From 8699730686485e4438966b3bd31b22c227b7b143 Mon Sep 17 00:00:00 2001 From: Anna Quinlan Date: Tue, 3 Jan 2023 10:15:36 -0800 Subject: [PATCH 16/25] Add test for autobolus clamping --- LoopTests/Managers/LoopDataManagerTests.swift | 65 +++++++++++++++++-- LoopTests/Mock Stores/MockCarbStore.swift | 2 +- LoopTests/Mock Stores/MockDoseStore.swift | 11 +++- LoopTests/Mock Stores/MockGlucoseStore.swift | 8 +-- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index e22560ce8b..19511e84e0 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -21,6 +21,28 @@ enum DataManagerTestType { case lowAndFallingWithCOB case lowWithLowTreatment case highAndFalling + /// uses fixtures for .highAndRisingWithCOB with a low max bolus and dosing set to autobolus + case autoBolusIOBClamping +} + +extension DataManagerTestType { + var dosingStrategy: AutomaticDosingStrategy { + switch self { + case .autoBolusIOBClamping: + return .automaticBolus + default: + return .tempBasalOnly + } + } + + var maxBolus: Double { + switch self { + case .autoBolusIOBClamping: + return 5 + default: + return LoopDataManagerDosingTests.defaultMaxBolus + } + } } extension TimeZone { @@ -56,7 +78,7 @@ class LoopDataManagerDosingTests: XCTestCase { // MARK: Settings let maxBasalRate = 5.0 - let maxBolus = 10.0 + static let defaultMaxBolus = 10.0 var suspendThreshold: GlucoseThreshold { return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75) @@ -85,8 +107,9 @@ class LoopDataManagerDosingTests: XCTestCase { glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, basalRateSchedule: basalRateSchedule, maximumBasalRatePerHour: maxBasalRate, - maximumBolus: maxBolus, - suspendThreshold: suspendThreshold + maximumBolus: test.maxBolus, + suspendThreshold: suspendThreshold, + automaticDosingStrategy: test.dosingStrategy ) let doseStore = MockDoseStore(for: test) @@ -507,7 +530,7 @@ class LoopDataManagerDosingTests: XCTestCase { dosingEnabled: false, glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, maximumBasalRatePerHour: maxBasalRate, - maximumBolus: maxBolus, + maximumBolus: Self.defaultMaxBolus, suspendThreshold: suspendThreshold ) @@ -558,6 +581,40 @@ class LoopDataManagerDosingTests: XCTestCase { XCTAssertNil(mockDelegate.recommendation) } + + func testAutoBolusMaxIOBClamping() { + /// `maximumBolus` is set to clamp the automatic dose + /// Autobolus without clamping: 0.65 U. Clamped recommendation: 0.2 U. + setUp(for: .autoBolusIOBClamping) + + let updateGroup = DispatchGroup() + updateGroup.enter() + + var insulinOnBoard: InsulinValue? + var recommendedBolus: Double? + self.loopDataManager.getLoopState { _, state in + insulinOnBoard = state.insulinOnBoard + recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits + updateGroup.leave() + } + updateGroup.wait() + + XCTAssertEqual(recommendedBolus!, 0.2, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.5) + + /// Set the `maximumBolus` so there's no clamping + updateGroup.enter() + self.loopDataManager.mutateSettings { settings in settings.maximumBolus = Self.defaultMaxBolus } + self.loopDataManager.getLoopState { _, state in + insulinOnBoard = state.insulinOnBoard + recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits + updateGroup.leave() + } + updateGroup.wait() + + XCTAssertEqual(recommendedBolus!, 0.65, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.5) + } } extension LoopDataManagerDosingTests { diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index f61fcd70ed..d4f6bc7990 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -124,7 +124,7 @@ extension MockCarbStore { return "flat_and_stable_carb_effect" case .highAndStable: return "high_and_stable_carb_effect" - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return "high_and_rising_with_cob_carb_effect" case .lowAndFallingWithCOB: return "low_and_falling_carb_effect" diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index 20d4892db0..5953c2e82c 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -60,7 +60,12 @@ class MockDoseStore: DoseStoreProtocol { } func insulinOnBoard(at date: Date, completion: @escaping (DoseStoreResult) -> Void) { - completion(.failure(.configurationError)) + switch testType { + case .highAndRisingWithCOB, .autoBolusIOBClamping: + completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.5))) + default: + completion(.failure(.configurationError)) + } } func generateDiagnosticReport(_ completion: @escaping (String) -> Void) { @@ -112,7 +117,7 @@ class MockDoseStore: DoseStoreProtocol { return dateFormatter.date(from: "2020-08-11T20:45:02")! case .highAndStable: return dateFormatter.date(from: "2020-08-12T12:39:22")! - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return dateFormatter.date(from: "2020-08-11T21:48:17")! case .lowAndFallingWithCOB: return dateFormatter.date(from: "2020-08-11T22:06:06")! @@ -140,7 +145,7 @@ extension MockDoseStore { return "flat_and_stable_insulin_effect" case .highAndStable: return "high_and_stable_insulin_effect" - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return "high_and_rising_with_cob_insulin_effect" case .lowAndFallingWithCOB: return "low_and_falling_insulin_effect" diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index 2a5e1d2123..d5f4194ff4 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -112,7 +112,7 @@ extension MockGlucoseStore { return "flat_and_stable_counteraction_effect" case .highAndStable: return "high_and_stable_counteraction_effect" - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return "high_and_rising_with_cob_counteraction_effect" case .lowAndFallingWithCOB: return "low_and_falling_counteraction_effect" @@ -129,7 +129,7 @@ extension MockGlucoseStore { return "flat_and_stable_momentum_effect" case .highAndStable: return "high_and_stable_momentum_effect" - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return "high_and_rising_with_cob_momentum_effect" case .lowAndFallingWithCOB: return "low_and_falling_momentum_effect" @@ -146,7 +146,7 @@ extension MockGlucoseStore { return dateFormatter.date(from: "2020-08-11T20:45:02")! case .highAndStable: return dateFormatter.date(from: "2020-08-12T12:39:22")! - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return dateFormatter.date(from: "2020-08-11T21:48:17")! case .lowAndFallingWithCOB: return dateFormatter.date(from: "2020-08-11T22:06:06")! @@ -163,7 +163,7 @@ extension MockGlucoseStore { return 123.42849966275706 case .highAndStable: return 200.0 - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping: return 129.93174411197853 case .lowAndFallingWithCOB: return 75.10768374646841 From 3c7c65b0871ea884e62cd27d4799d2580ec19f78 Mon Sep 17 00:00:00 2001 From: Anna Quinlan Date: Tue, 3 Jan 2023 10:17:58 -0800 Subject: [PATCH 17/25] Improve readability of dose clamping logic I unified the check into 1 if-statement, changed the `checkAutomaticDosing` variable name so it was more descriptive, and changed the logic so it's clear that `minCorrectionUnits` is being subtracted from --- Loop/Managers/DoseMath.swift | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 88e3c4fcca..5580ef38e1 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -329,19 +329,17 @@ extension Collection where Element: GlucoseValue { minTarget: minGlucoseTargets.lowerBound, units: units ) - } else if eventual.quantity > eventualGlucoseTargets.upperBound, - var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose - { - // Limit automatic dosing to prevent insulinOnBoard > automaticDosingIOBLimit - if let automaticDosingIOBLimit = automaticDosingIOBLimit, - let insulinOnBoard = insulinOnBoard, - minCorrectionUnits > 0 + } else if eventual.quantity > eventualGlucoseTargets.upperBound, var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose { + /// Don't allow the correction units + current `insulinOnBoard` to go over `automaticDosingIOBLimit` + if + let automaticDosingIOBLimit, + let insulinOnBoard, + minCorrectionUnits > 0, + insulinOnBoard + minCorrectionUnits > automaticDosingIOBLimit { - let checkAutomaticDosing = automaticDosingIOBLimit - (insulinOnBoard + minCorrectionUnits) - if checkAutomaticDosing < 0 { - // TO DO - nice to have logging but not required - minCorrectionUnits = Swift.max(minCorrectionUnits+checkAutomaticDosing, 0) - } + let unitsOverAutomaticDosingLimit = (insulinOnBoard + minCorrectionUnits) - automaticDosingIOBLimit + // TO DO - nice to have logging but not required + minCorrectionUnits = Swift.max(minCorrectionUnits - unitsOverAutomaticDosingLimit, 0) } return .aboveRange( From 61bea9d1ebbfa242acffc00dfe2e0e63846ad7cf Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 4 Jan 2023 14:53:55 -0800 Subject: [PATCH 18/25] DoseMathTests: use non-zero value for insulinOnBoard --- DoseMathTests/DoseMathTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index d11878e04b..79ea580d15 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -111,7 +111,11 @@ class RecommendTempBasalTests: XCTestCase { return 2.0 * maxBolus } - var insulinOnBoard: Double = 0.0 + // Tests that were in place before the addition of automaticDosingIOBLimit + // should all still succeed so long as + // insulinOnBoard < automaticDosingIOBLimit - (dose returned by test) + // The test with highest dose is a bolus of 2.3 U + var insulinOnBoard: Double = 7.69 func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") From 01c775c8c0efa529a7853f9872e48990981289fd Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 4 Jan 2023 21:16:27 -0800 Subject: [PATCH 19/25] DoseMathTests: move insulinOnBoard internal to test functions --- DoseMathTests/DoseMathTests.swift | 52 +++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 79ea580d15..f4bb2bd6a1 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -111,14 +111,9 @@ class RecommendTempBasalTests: XCTestCase { return 2.0 * maxBolus } - // Tests that were in place before the addition of automaticDosingIOBLimit - // should all still succeed so long as - // insulinOnBoard < automaticDosingIOBLimit - (dose returned by test) - // The test with highest dose is a bolus of 2.3 U - var insulinOnBoard: Double = 7.69 - func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + let insulinOnBoard = 0.0 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -138,6 +133,7 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + let insulinOnBoard = 0.0 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -157,6 +153,7 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeAutomaticBolusing() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + let insulinOnBoard = 0.0 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -177,6 +174,7 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeOverrideActive() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -198,6 +196,7 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeOverrideActiveAutomaticBolusing() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -221,6 +220,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -265,6 +265,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndInRangeAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -312,6 +313,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndInRangeAutomaticBolusWithOverride() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -360,6 +362,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") + let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -403,6 +406,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndInRangeExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") + let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -446,6 +450,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndInRangeAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") + let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -492,6 +497,7 @@ class RecommendTempBasalTests: XCTestCase { func testCorrectLowAtMin() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min") + let insulinOnBoard = 7.69 // Cancel existing dose let lastTempBasal = DoseEntry( @@ -536,6 +542,7 @@ class RecommendTempBasalTests: XCTestCase { func testCorrectLowAtMinAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min") + let insulinOnBoard = 7.69 // Cancel existing dose let lastTempBasal = DoseEntry( @@ -583,6 +590,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndLow() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -603,6 +611,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndLowAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") + let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -625,6 +634,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndLowExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -645,6 +655,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -689,6 +700,7 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndHighAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -735,6 +747,7 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -755,6 +768,7 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHighAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -776,6 +790,7 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHighAutomaticBolusWithOverride() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -827,7 +842,8 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndFalling() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") - + let insulinOnBoard = 7.69 + let insulinModel = WalshInsulinModel(actionDuration: insulinActionDuration, delay: 0) let dose = glucose.recommendedTempBasal( @@ -849,6 +865,7 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndFallingAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") + let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -870,6 +887,7 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndFallingExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -890,6 +908,7 @@ class RecommendTempBasalTests: XCTestCase { func testInRangeAndRisingAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") + let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -911,6 +930,7 @@ class RecommendTempBasalTests: XCTestCase { func testInRangeAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -931,6 +951,7 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") + let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -970,6 +991,7 @@ class RecommendTempBasalTests: XCTestCase { func testVeryLowAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_very_low_end_in_range") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -990,6 +1012,7 @@ class RecommendTempBasalTests: XCTestCase { func testRiseAfterExponentialModelDIA() { let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast_after_6_hours") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1009,6 +1032,7 @@ class RecommendTempBasalTests: XCTestCase { func testRiseAfterDIA() { let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast") + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1029,6 +1053,7 @@ class RecommendTempBasalTests: XCTestCase { func testNoInputGlucose() { let glucose: [GlucoseFixtureValue] = [] + let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1050,6 +1075,7 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHighAutomaticBolusNoLimit() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -1074,7 +1100,7 @@ class RecommendTempBasalTests: XCTestCase { // max allowed dose will be 0.5 units let maxAutoCorrection = 0.5 - insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + let insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -1101,7 +1127,7 @@ class RecommendTempBasalTests: XCTestCase { // for this test use partialApplicationFactor of 0.5, instead of 1.0 let maxAutoCorrection = 0.5 let partialApplicationFactor = 0.5 - insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + let insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -1125,7 +1151,7 @@ class RecommendTempBasalTests: XCTestCase { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") // no automatic dose - insulinOnBoard = automaticDosingIOBLimit + 0.1 + let insulinOnBoard = automaticDosingIOBLimit + 0.1 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -1153,7 +1179,7 @@ class RecommendTempBasalTests: XCTestCase { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") // no limit - insulinOnBoard = 0.0 + let insulinOnBoard = 0.0 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1178,7 +1204,7 @@ class RecommendTempBasalTests: XCTestCase { // max allowed dose will be 0.5 units, temp basal twice that let maxAutoCorrection = 0.5 let limitedBasalRate = scheduledBasalRate + 2.0 * maxAutoCorrection - insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection + let insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1201,7 +1227,7 @@ class RecommendTempBasalTests: XCTestCase { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") // no automatic dose - insulinOnBoard = automaticDosingIOBLimit + 0.1 + let insulinOnBoard = automaticDosingIOBLimit + 0.1 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, From c6323df2e8e72244f42c6f6d4f3407ddf48e98c7 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 18 Feb 2023 13:50:34 -0600 Subject: [PATCH 20/25] Move IOB limit handling into recommendedAutomaticDose, and recommendedTempBasal methods --- DoseMathTests/DoseMathTests.swift | 392 +++--------------- Loop/Managers/DoseMath.swift | 61 +-- Loop/Managers/LoopDataManager.swift | 20 +- Loop/Models/LoopError.swift | 7 +- LoopTests/Managers/LoopDataManagerTests.swift | 56 ++- LoopTests/Mock Stores/MockCarbStore.swift | 2 +- LoopTests/Mock Stores/MockDoseStore.swift | 12 +- LoopTests/Mock Stores/MockGlucoseStore.swift | 8 +- 8 files changed, 150 insertions(+), 408 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index f4bb2bd6a1..86c878c8d8 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -55,7 +55,15 @@ class RecommendTempBasalTests: XCTestCase { fileprivate let maxBasalRate = 3.0 - fileprivate let maxBolus = 5.0 + fileprivate let maxBolus = 12.5 + + var automaticDosingIOBLimit: Double { + return 2.0 * maxBolus + } + + var maxAutomaticBolus: Double { + return maxBolus * 0.4 + } fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } @@ -107,13 +115,8 @@ class RecommendTempBasalTests: XCTestCase { return TimeInterval(hours: 4) } - var automaticDosingIOBLimit: Double { - return 2.0 * maxBolus - } - func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") - let insulinOnBoard = 0.0 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -123,9 +126,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -133,7 +134,6 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") - let insulinOnBoard = 0.0 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -143,9 +143,7 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -153,7 +151,6 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeAutomaticBolusing() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") - let insulinOnBoard = 0.0 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -164,9 +161,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -174,7 +169,6 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeOverrideActive() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -185,9 +179,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: true ) XCTAssertEqual(0.8, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -196,7 +188,6 @@ class RecommendTempBasalTests: XCTestCase { func testNoChangeOverrideActiveAutomaticBolusing() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") - let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -208,9 +199,7 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: true ) XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -220,7 +209,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") - let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -230,9 +218,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -254,9 +240,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -265,7 +249,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndInRangeAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") - let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -276,9 +259,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -301,9 +282,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -313,7 +292,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndInRangeAutomaticBolusWithOverride() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") - let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -325,9 +303,7 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: true ) XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -352,9 +328,7 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: lastTempBasal, - isBasalRateScheduleOverrideActive: true, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: true ) XCTAssertNil(dose) @@ -362,7 +336,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") - let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -372,9 +345,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -395,9 +366,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -406,7 +375,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndInRangeExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") - let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -416,9 +384,7 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -439,9 +405,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -450,7 +414,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndInRangeAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") - let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -461,9 +424,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -485,9 +446,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -497,7 +456,6 @@ class RecommendTempBasalTests: XCTestCase { func testCorrectLowAtMin() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min") - let insulinOnBoard = 7.69 // Cancel existing dose let lastTempBasal = DoseEntry( @@ -516,9 +474,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -532,9 +488,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -542,7 +496,6 @@ class RecommendTempBasalTests: XCTestCase { func testCorrectLowAtMinAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min") - let insulinOnBoard = 7.69 // Cancel existing dose let lastTempBasal = DoseEntry( @@ -562,9 +515,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -580,9 +531,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -590,7 +539,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndLow() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -600,9 +548,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -611,7 +557,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndLowAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") - let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -622,9 +567,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -634,7 +577,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartHighEndLowExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -644,9 +586,7 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -655,7 +595,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") - let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -665,9 +604,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -689,9 +626,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.unitsPerHour) @@ -700,7 +635,6 @@ class RecommendTempBasalTests: XCTestCase { func testStartLowEndHighAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") - let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -711,9 +645,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -735,9 +667,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: lastTempBasal, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: lastTempBasal ) XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) @@ -747,7 +677,6 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -757,9 +686,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(3.0, dose!.unitsPerHour) @@ -768,7 +695,6 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHighAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -779,9 +705,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose!.basalAdjustment) @@ -790,7 +714,6 @@ class RecommendTempBasalTests: XCTestCase { func testFlatAndHighAutomaticBolusWithOverride() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - let insulinOnBoard = 7.69 var dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -802,9 +725,7 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: nil, - isBasalRateScheduleOverrideActive: true, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: true ) XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -830,9 +751,7 @@ class RecommendTempBasalTests: XCTestCase { maxAutomaticBolus: 5, partialApplicationFactor: 0.5, lastTempBasal: lastTempBasal, - isBasalRateScheduleOverrideActive: true, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: true ) XCTAssertNil(dose!.basalAdjustment) @@ -842,7 +761,6 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndFalling() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") - let insulinOnBoard = 7.69 let insulinModel = WalshInsulinModel(actionDuration: insulinActionDuration, delay: 0) @@ -854,9 +772,7 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(1.425, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -865,7 +781,6 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndFallingAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") - let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -876,9 +791,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose!.basalAdjustment) @@ -887,7 +800,6 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndFallingExponentialModel() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -897,9 +809,7 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(2.68, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -908,7 +818,6 @@ class RecommendTempBasalTests: XCTestCase { func testInRangeAndRisingAutomaticBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") - let insulinOnBoard = 7.69 let dose = glucose.recommendedAutomaticDose( to: glucoseTargetRange, @@ -919,9 +828,7 @@ class RecommendTempBasalTests: XCTestCase { basalRates: basalRateSchedule, maxAutomaticBolus: 5, partialApplicationFactor: 0.5, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose!.basalAdjustment) @@ -930,7 +837,6 @@ class RecommendTempBasalTests: XCTestCase { func testInRangeAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -940,9 +846,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(1.60, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -951,7 +855,6 @@ class RecommendTempBasalTests: XCTestCase { func testHighAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - let insulinOnBoard = 7.69 var dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -961,9 +864,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(3.0, dose!.unitsPerHour) @@ -980,9 +881,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(2.975, dose!.unitsPerHour, accuracy: 1.0 / 40.0) @@ -991,7 +890,6 @@ class RecommendTempBasalTests: XCTestCase { func testVeryLowAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_very_low_end_in_range") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1001,9 +899,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertEqual(0.0, dose!.unitsPerHour) @@ -1012,7 +908,6 @@ class RecommendTempBasalTests: XCTestCase { func testRiseAfterExponentialModelDIA() { let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast_after_6_hours") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1022,9 +917,7 @@ class RecommendTempBasalTests: XCTestCase { model: exponentialInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -1032,7 +925,6 @@ class RecommendTempBasalTests: XCTestCase { func testRiseAfterDIA() { let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast") - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1042,9 +934,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) @@ -1053,7 +943,6 @@ class RecommendTempBasalTests: XCTestCase { func testNoInputGlucose() { let glucose: [GlucoseFixtureValue] = [] - let insulinOnBoard = 7.69 let dose = glucose.recommendedTempBasal( to: glucoseTargetRange, @@ -1062,184 +951,7 @@ class RecommendTempBasalTests: XCTestCase { model: walshInsulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose) - } - - // Next four tests, recommended bolus is 2.30 U (no limit) - // adjust IOB to modify AutomaticBolus - - func testFlatAndHighAutomaticBolusNoLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - let insulinOnBoard = 7.69 - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: 1.0, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose!.basalAdjustment) - XCTAssertEqual(2.30, dose!.bolusUnits!, accuracy: 1.0 / 40.0) - } - - func testFlatAndHighAutomaticBolusWithLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - // max allowed dose will be 0.5 units - let maxAutoCorrection = 0.5 - let insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: 1.0, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose!.basalAdjustment) - XCTAssertEqual(maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) - } - - func testFlatAndHighAutomaticBolusWithLimitPAF() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - // max allowed dose will be 0.5 units - // for this test use partialApplicationFactor of 0.5, instead of 1.0 - let maxAutoCorrection = 0.5 - let partialApplicationFactor = 0.5 - let insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: partialApplicationFactor, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose!.basalAdjustment) - XCTAssertEqual(partialApplicationFactor * maxAutoCorrection, dose!.bolusUnits!, accuracy: 1.0 / 40.0) - } - - func testFlatAndHighAutomaticBolusFullLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - - // no automatic dose - let insulinOnBoard = automaticDosingIOBLimit + 0.1 - - let dose = glucose.recommendedAutomaticDose( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxAutomaticBolus: maxBolus, - partialApplicationFactor: 1.0, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertNil(dose) - } - - // Next three test recommended TempBasal is 3.0 U/hr (no limit) - // Adjust IOB to modify automatic TempBasal - let noLimitBasalRate = 3.0 - let scheduledBasalRate = 0.8 - - func testHighAndRisingTempBasalNoLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - - // no limit - let insulinOnBoard = 0.0 - - let dose = glucose.recommendedTempBasal( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: self.insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertEqual(noLimitBasalRate, dose!.unitsPerHour) - XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) - } - - func testHighAndRisingTempBasalWithLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - - // max allowed dose will be 0.5 units, temp basal twice that - let maxAutoCorrection = 0.5 - let limitedBasalRate = scheduledBasalRate + 2.0 * maxAutoCorrection - let insulinOnBoard = automaticDosingIOBLimit - maxAutoCorrection - - let dose = glucose.recommendedTempBasal( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: self.insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit - ) - - XCTAssertEqual(limitedBasalRate, dose!.unitsPerHour) - XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) - } - - func testHighAndRisingTempBasalFullLimit() { - let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - - // no automatic dose - let insulinOnBoard = automaticDosingIOBLimit + 0.1 - - let dose = glucose.recommendedTempBasal( - to: glucoseTargetRange, - at: glucose.first!.startDate, - suspendThreshold: suspendThreshold.quantity, - sensitivity: self.insulinSensitivitySchedule, - model: exponentialInsulinModel, - basalRates: basalRateSchedule, - maxBasalRate: maxBasalRate, - lastTempBasal: nil, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + lastTempBasal: nil ) XCTAssertNil(dose) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 5580ef38e1..0ed9a2823c 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -122,7 +122,7 @@ extension InsulinCorrection { let partialDose = units * partialApplicationFactor - return Swift.min(Swift.max(0, volumeRounder?(partialDose) ?? partialDose),maxBolusUnits) + return Swift.min(Swift.max(0, volumeRounder?(partialDose) ?? partialDose),volumeRounder?(maxBolusUnits) ?? maxBolusUnits) } } @@ -234,9 +234,7 @@ extension Collection where Element: GlucoseValue { at date: Date, suspendThreshold: HKQuantity, sensitivity: HKQuantity, - model: InsulinModel, - insulinOnBoard: Double?, - automaticDosingIOBLimit: Double? + model: InsulinModel ) -> InsulinCorrection? { var minGlucose: GlucoseValue? var eventualGlucose: GlucoseValue? @@ -300,24 +298,24 @@ extension Collection where Element: GlucoseValue { minCorrectionUnits = correctionUnits } - guard let eventual = eventualGlucose, let min = minGlucose else { + guard let eventualGlucose, let minGlucose else { return nil } // Choose either the minimum glucose or eventual glucose as the correction delta - let minGlucoseTargets = correctionRange.quantityRange(at: min.startDate) - let eventualGlucoseTargets = correctionRange.quantityRange(at: eventual.startDate) + let minGlucoseTargets = correctionRange.quantityRange(at: minGlucose.startDate) + let eventualGlucoseTargets = correctionRange.quantityRange(at: eventualGlucose.startDate) // Treat the mininum glucose when both are below range - if min.quantity < minGlucoseTargets.lowerBound && - eventual.quantity < eventualGlucoseTargets.lowerBound + if minGlucose.quantity < minGlucoseTargets.lowerBound && + eventualGlucose.quantity < eventualGlucoseTargets.lowerBound { - let time = min.startDate.timeIntervalSince(date) + let time = minGlucose.startDate.timeIntervalSince(date) // For 0 <= time <= effectDelay, assume a small amount effected. This will result in large (negative) unit recommendation rather than no recommendation at all. let percentEffected = Swift.max(.ulpOfOne, 1 - model.percentEffectRemaining(at: time)) guard let units = insulinCorrectionUnits( - fromValue: min.quantity.doubleValue(for: unit), + fromValue: minGlucose.quantity.doubleValue(for: unit), toValue: minGlucoseTargets.averageValue(for: unit), effectedSensitivity: sensitivityValue * percentEffected ) else { @@ -325,25 +323,15 @@ extension Collection where Element: GlucoseValue { } return .entirelyBelowRange( - min: min, + min: minGlucose, minTarget: minGlucoseTargets.lowerBound, units: units ) - } else if eventual.quantity > eventualGlucoseTargets.upperBound, var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose { - /// Don't allow the correction units + current `insulinOnBoard` to go over `automaticDosingIOBLimit` - if - let automaticDosingIOBLimit, - let insulinOnBoard, - minCorrectionUnits > 0, - insulinOnBoard + minCorrectionUnits > automaticDosingIOBLimit - { - let unitsOverAutomaticDosingLimit = (insulinOnBoard + minCorrectionUnits) - automaticDosingIOBLimit - // TO DO - nice to have logging but not required - minCorrectionUnits = Swift.max(minCorrectionUnits - unitsOverAutomaticDosingLimit, 0) - } - + } else if eventualGlucose.quantity > eventualGlucoseTargets.upperBound, + let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose + { return .aboveRange( - min: min, + min: minGlucose, correcting: correctingGlucose, minTarget: eventualGlucoseTargets.lowerBound, units: minCorrectionUnits @@ -383,18 +371,14 @@ extension Collection where Element: GlucoseValue { rateRounder: ((Double) -> Double)? = nil, isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), - continuationInterval: TimeInterval = .minutes(11), - insulinOnBoard: Double?, - automaticDosingIOBLimit: Double? + continuationInterval: TimeInterval = .minutes(11) ) -> TempBasalRecommendation? { let correction = self.insulinCorrection( to: correctionRange, at: date, suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), - model: model, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + model: model ) let scheduledBasalRate = basalRates.value(at: date) @@ -440,6 +424,7 @@ extension Collection where Element: GlucoseValue { /// - isBasalRateScheduleOverrideActive: A flag describing whether a basal rate schedule override is in progress /// - duration: The duration of the temporary basal /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command + /// - insulinOnBoard: /// - Returns: The recommended dosing, or nil if no dose adjustment recommended func recommendedAutomaticDose( to correctionRange: GlucoseRangeSchedule, @@ -455,18 +440,14 @@ extension Collection where Element: GlucoseValue { rateRounder: ((Double) -> Double)? = nil, isBasalRateScheduleOverrideActive: Bool = false, duration: TimeInterval = .minutes(30), - continuationInterval: TimeInterval = .minutes(11), - insulinOnBoard: Double?, - automaticDosingIOBLimit: Double? + continuationInterval: TimeInterval = .minutes(11) ) -> AutomaticDoseRecommendation? { guard let correction = self.insulinCorrection( to: correctionRange, at: date, suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), - model: model, - insulinOnBoard: insulinOnBoard, - automaticDosingIOBLimit: automaticDosingIOBLimit + model: model ) else { return nil } @@ -536,9 +517,7 @@ extension Collection where Element: GlucoseValue { at: date, suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, sensitivity: sensitivity.quantity(at: date), - model: model, - insulinOnBoard: nil, - automaticDosingIOBLimit: nil + model: model ) else { return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin) } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index a76e0659e0..ccb7c559d0 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1604,6 +1604,10 @@ extension LoopDataManager { errors.append(.missingDataError(.insulinEffectIncludingPendingInsulin)) } + if self.insulinOnBoard == nil { + errors.append(.missingDataError(.activeInsulin)) + } + dosingDecision.appendErrors(errors) if let error = errors.first { logger.error("%{public}@", String(describing: error)) @@ -1652,6 +1656,9 @@ extension LoopDataManager { return self.delegate?.roundBolusVolume(units: units) ?? units } + let iobHeadroom = automaticDosingIOBLimit - self.insulinOnBoard!.value + let maxAutomaticBolus = min(iobHeadroom, maxBolus! * LoopConstants.bolusPartialApplicationFactor) + dosingRecommendation = predictedGlucose.recommendedAutomaticDose( to: glucoseTargetRange!, at: predictedGlucose[0].startDate, @@ -1659,16 +1666,17 @@ extension LoopDataManager { sensitivity: insulinSensitivity!, model: doseStore.insulinModelProvider.model(for: pumpInsulinType), basalRates: basalRates!, - maxAutomaticBolus: maxBolus! * LoopConstants.bolusPartialApplicationFactor, + maxAutomaticBolus: maxAutomaticBolus, partialApplicationFactor: LoopConstants.bolusPartialApplicationFactor * self.timeBasedDoseApplicationFactor, lastTempBasal: lastTempBasal, volumeRounder: volumeRounder, rateRounder: rateRounder, - isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, - insulinOnBoard: self.insulinOnBoard?.value, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true ) case .tempBasalOnly: + + // TODO: Adjust maxBasalRate so that max iob is not exceeded. + let temp = predictedGlucose.recommendedTempBasal( to: glucoseTargetRange!, at: predictedGlucose[0].startDate, @@ -1679,9 +1687,7 @@ extension LoopDataManager { maxBasalRate: maxBasal!, lastTempBasal: lastTempBasal, rateRounder: rateRounder, - isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true, - insulinOnBoard: self.insulinOnBoard?.value, - automaticDosingIOBLimit: automaticDosingIOBLimit + isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true ) dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp) } diff --git a/Loop/Models/LoopError.swift b/Loop/Models/LoopError.swift index babbc8bbb9..015d5cc05c 100644 --- a/Loop/Models/LoopError.swift +++ b/Loop/Models/LoopError.swift @@ -43,6 +43,7 @@ enum MissingDataErrorDetail: String, Codable { case momentumEffect case carbEffect case insulinEffect + case activeInsulin case insulinEffectIncludingPendingInsulin var localizedDetail: String { @@ -55,6 +56,8 @@ enum MissingDataErrorDetail: String, Codable { return NSLocalizedString("Carb effects", comment: "Details for missing data error when carb effects are missing") case .insulinEffect: return NSLocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects are missing") + case .activeInsulin: + return NSLocalizedString("Active Insulin", comment: "Details for missing data error when active insulin amount is missing") case .insulinEffectIncludingPendingInsulin: return NSLocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects including pending insulin are missing") } @@ -68,9 +71,7 @@ enum MissingDataErrorDetail: String, Codable { return nil case .carbEffect: return nil - case .insulinEffect: - return nil - case .insulinEffectIncludingPendingInsulin: + case .insulinEffect, .activeInsulin, .insulinEffectIncludingPendingInsulin: return nil } } diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index 19511e84e0..9dfe9763d4 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -23,6 +23,7 @@ enum DataManagerTestType { case highAndFalling /// uses fixtures for .highAndRisingWithCOB with a low max bolus and dosing set to autobolus case autoBolusIOBClamping + case tempBasalIOBClamping } extension DataManagerTestType { @@ -327,8 +328,8 @@ class LoopDataManagerDosingTests: XCTestCase { self.recommendation = automaticDose.recommendation completion(error) } - func roundBasalRate(unitsPerHour: Double) -> Double { unitsPerHour } - func roundBolusVolume(units: Double) -> Double { units } + func roundBasalRate(unitsPerHour: Double) -> Double { Double(Int(unitsPerHour / 0.05)) * 0.05 } + func roundBolusVolume(units: Double) -> Double { Double(Int(units / 0.05)) * 0.05 } var pumpManagerStatus: PumpManagerStatus? var cgmManagerStatus: CGMManagerStatus? var pumpStatusHighlight: DeviceStatusHighlight? @@ -581,12 +582,15 @@ class LoopDataManagerDosingTests: XCTestCase { XCTAssertNil(mockDelegate.recommendation) } - func testAutoBolusMaxIOBClamping() { /// `maximumBolus` is set to clamp the automatic dose /// Autobolus without clamping: 0.65 U. Clamped recommendation: 0.2 U. setUp(for: .autoBolusIOBClamping) - + + // This sets up dose rounding + let delegate = MockDelegate() + loopDataManager.delegate = delegate + let updateGroup = DispatchGroup() updateGroup.enter() @@ -599,8 +603,8 @@ class LoopDataManagerDosingTests: XCTestCase { } updateGroup.wait() - XCTAssertEqual(recommendedBolus!, 0.2, accuracy: 0.01) - XCTAssertEqual(insulinOnBoard?.value, 9.5) + XCTAssertEqual(recommendedBolus!, 0.5, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.47) /// Set the `maximumBolus` so there's no clamping updateGroup.enter() @@ -613,8 +617,46 @@ class LoopDataManagerDosingTests: XCTestCase { updateGroup.wait() XCTAssertEqual(recommendedBolus!, 0.65, accuracy: 0.01) - XCTAssertEqual(insulinOnBoard?.value, 9.5) + XCTAssertEqual(insulinOnBoard?.value, 9.47) + } + + func testTempBasalMaxIOBClamping() { + /// `maximumBolus` is set to clamp the automatic dose + /// Without clamping: 4.25 U/hr. Clamped recommendation: 4.2 U/hr. + setUp(for: .tempBasalIOBClamping) + + // This sets up dose rounding + let delegate = MockDelegate() + loopDataManager.delegate = delegate + + let updateGroup = DispatchGroup() + updateGroup.enter() + + var insulinOnBoard: InsulinValue? + var recommendedBasal: TempBasalRecommendation? + self.loopDataManager.getLoopState { _, state in + insulinOnBoard = state.insulinOnBoard + recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment + updateGroup.leave() + } + updateGroup.wait() + + XCTAssertEqual(recommendedBasal!.unitsPerHour, 4.2, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.87) + + /// Set the `maximumBolus` so there's no clamping + updateGroup.enter() + self.loopDataManager.getLoopState { _, state in + insulinOnBoard = state.insulinOnBoard + recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment + updateGroup.leave() + } + updateGroup.wait() + + XCTAssertEqual(recommendedBasal!.unitsPerHour, 4.2, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.87) } + } extension LoopDataManagerDosingTests { diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index d4f6bc7990..1d95d92a1c 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -124,7 +124,7 @@ extension MockCarbStore { return "flat_and_stable_carb_effect" case .highAndStable: return "high_and_stable_carb_effect" - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return "high_and_rising_with_cob_carb_effect" case .lowAndFallingWithCOB: return "low_and_falling_carb_effect" diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index 5953c2e82c..24bcbac33c 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -61,10 +61,12 @@ class MockDoseStore: DoseStoreProtocol { func insulinOnBoard(at date: Date, completion: @escaping (DoseStoreResult) -> Void) { switch testType { - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .flatAndStable, .highAndFalling, .highAndStable, .lowAndFallingWithCOB, .lowWithLowTreatment: completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.5))) - default: - completion(.failure(.configurationError)) + case .autoBolusIOBClamping: + completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.47))) + case .tempBasalIOBClamping: + completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.87))) } } @@ -117,7 +119,7 @@ class MockDoseStore: DoseStoreProtocol { return dateFormatter.date(from: "2020-08-11T20:45:02")! case .highAndStable: return dateFormatter.date(from: "2020-08-12T12:39:22")! - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return dateFormatter.date(from: "2020-08-11T21:48:17")! case .lowAndFallingWithCOB: return dateFormatter.date(from: "2020-08-11T22:06:06")! @@ -145,7 +147,7 @@ extension MockDoseStore { return "flat_and_stable_insulin_effect" case .highAndStable: return "high_and_stable_insulin_effect" - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return "high_and_rising_with_cob_insulin_effect" case .lowAndFallingWithCOB: return "low_and_falling_insulin_effect" diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index d5f4194ff4..39adfc16bf 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -112,7 +112,7 @@ extension MockGlucoseStore { return "flat_and_stable_counteraction_effect" case .highAndStable: return "high_and_stable_counteraction_effect" - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return "high_and_rising_with_cob_counteraction_effect" case .lowAndFallingWithCOB: return "low_and_falling_counteraction_effect" @@ -129,7 +129,7 @@ extension MockGlucoseStore { return "flat_and_stable_momentum_effect" case .highAndStable: return "high_and_stable_momentum_effect" - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return "high_and_rising_with_cob_momentum_effect" case .lowAndFallingWithCOB: return "low_and_falling_momentum_effect" @@ -146,7 +146,7 @@ extension MockGlucoseStore { return dateFormatter.date(from: "2020-08-11T20:45:02")! case .highAndStable: return dateFormatter.date(from: "2020-08-12T12:39:22")! - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return dateFormatter.date(from: "2020-08-11T21:48:17")! case .lowAndFallingWithCOB: return dateFormatter.date(from: "2020-08-11T22:06:06")! @@ -163,7 +163,7 @@ extension MockGlucoseStore { return 123.42849966275706 case .highAndStable: return 200.0 - case .highAndRisingWithCOB, .autoBolusIOBClamping: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return 129.93174411197853 case .lowAndFallingWithCOB: return 75.10768374646841 From caf2709ca3e165e27cfd3dbc4b72d61c038c514c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 18 Feb 2023 14:37:55 -0600 Subject: [PATCH 21/25] Temp basals limited by iob max --- Loop/Managers/LoopDataManager.swift | 15 ++++++++------- LoopTests/Managers/LoopDataManagerTests.swift | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index ccb7c559d0..2eb2beb034 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1564,8 +1564,8 @@ extension LoopDataManager { errors.append(.configurationError(.glucoseTargetRangeSchedule)) } - let basalRates = basalRateScheduleApplyingOverrideHistory - if basalRates == nil { + let basalRateSchedule = basalRateScheduleApplyingOverrideHistory + if basalRateSchedule == nil { errors.append(.configurationError(.basalRateSchedule)) } @@ -1649,6 +1649,7 @@ extension LoopDataManager { // automaticDosingIOBLimit calculated from the user entered maxBolus let automaticDosingIOBLimit = maxBolus! * 2.0 + let iobHeadroom = automaticDosingIOBLimit - self.insulinOnBoard!.value switch settings.automaticDosingStrategy { case .automaticBolus: @@ -1656,7 +1657,6 @@ extension LoopDataManager { return self.delegate?.roundBolusVolume(units: units) ?? units } - let iobHeadroom = automaticDosingIOBLimit - self.insulinOnBoard!.value let maxAutomaticBolus = min(iobHeadroom, maxBolus! * LoopConstants.bolusPartialApplicationFactor) dosingRecommendation = predictedGlucose.recommendedAutomaticDose( @@ -1665,7 +1665,7 @@ extension LoopDataManager { suspendThreshold: settings.suspendThreshold?.quantity, sensitivity: insulinSensitivity!, model: doseStore.insulinModelProvider.model(for: pumpInsulinType), - basalRates: basalRates!, + basalRates: basalRateSchedule!, maxAutomaticBolus: maxAutomaticBolus, partialApplicationFactor: LoopConstants.bolusPartialApplicationFactor * self.timeBasedDoseApplicationFactor, lastTempBasal: lastTempBasal, @@ -1675,7 +1675,8 @@ extension LoopDataManager { ) case .tempBasalOnly: - // TODO: Adjust maxBasalRate so that max iob is not exceeded. + let maxThirtyMinuteRateToKeepIOBBelowLimit = iobHeadroom / 2.0 // 30 minutes of a U/hr rate + let maxTempBasalRate = min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasal!) let temp = predictedGlucose.recommendedTempBasal( to: glucoseTargetRange!, @@ -1683,8 +1684,8 @@ extension LoopDataManager { suspendThreshold: settings.suspendThreshold?.quantity, sensitivity: insulinSensitivity!, model: doseStore.insulinModelProvider.model(for: pumpInsulinType), - basalRates: basalRates!, - maxBasalRate: maxBasal!, + basalRates: basalRateSchedule!, + maxBasalRate: maxTempBasalRate, lastTempBasal: lastTempBasal, rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index 9dfe9763d4..ff2f1d97b7 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -456,7 +456,7 @@ class LoopDataManagerDosingTests: XCTestCase { } loopDataManager.loop() wait(for: [exp], timeout: 1.0) - let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.577747629410191, duration: .minutes(30))) + let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.55, duration: .minutes(30))) XCTAssertEqual(delegate.recommendation, expectedAutomaticDoseRecommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) if dosingDecisionStore.dosingDecisions.count == 1 { @@ -480,7 +480,7 @@ class LoopDataManagerDosingTests: XCTestCase { } loopDataManager.loop() wait(for: [exp], timeout: 1.0) - let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.577747629410191, duration: .minutes(30))) + let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.55, duration: .minutes(30))) XCTAssertNil(delegate.recommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) XCTAssertEqual(dosingDecisionStore.dosingDecisions[0].reason, "loop") @@ -653,7 +653,7 @@ class LoopDataManagerDosingTests: XCTestCase { } updateGroup.wait() - XCTAssertEqual(recommendedBasal!.unitsPerHour, 4.2, accuracy: 0.01) + XCTAssertEqual(recommendedBasal!.unitsPerHour, 4.25, accuracy: 0.01) XCTAssertEqual(insulinOnBoard?.value, 9.87) } From 374160f4198ac98c3312f0ead4730216f8dbaf2a Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 18 Feb 2023 14:54:26 -0600 Subject: [PATCH 22/25] Cleanup --- DoseMathTests/DoseMathTests.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 86c878c8d8..5205955465 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -55,16 +55,6 @@ class RecommendTempBasalTests: XCTestCase { fileprivate let maxBasalRate = 3.0 - fileprivate let maxBolus = 12.5 - - var automaticDosingIOBLimit: Double { - return 2.0 * maxBolus - } - - var maxAutomaticBolus: Double { - return maxBolus * 0.4 - } - fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 } func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseFixtureValue] { From e0418409037f76b5ecb359224bd58814e32aeebb Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 18 Feb 2023 15:39:11 -0600 Subject: [PATCH 23/25] Remove unintentional edit --- Loop/Managers/DoseMath.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 0ed9a2823c..fb575a4385 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -424,7 +424,6 @@ extension Collection where Element: GlucoseValue { /// - isBasalRateScheduleOverrideActive: A flag describing whether a basal rate schedule override is in progress /// - duration: The duration of the temporary basal /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command - /// - insulinOnBoard: /// - Returns: The recommended dosing, or nil if no dose adjustment recommended func recommendedAutomaticDose( to correctionRange: GlucoseRangeSchedule, From 6b83d2fc82ede2eceb3145d7e632a03cd519c124 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sat, 18 Feb 2023 15:57:02 -0800 Subject: [PATCH 24/25] Fix maxThirtyMinuteRateToKeepIOBBelowLimit calculation --- Loop/Managers/LoopDataManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 2eb2beb034..7a7490999e 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1675,7 +1675,7 @@ extension LoopDataManager { ) case .tempBasalOnly: - let maxThirtyMinuteRateToKeepIOBBelowLimit = iobHeadroom / 2.0 // 30 minutes of a U/hr rate + let maxThirtyMinuteRateToKeepIOBBelowLimit = iobHeadroom * 2.0 // 30 minutes of a U/hr rate let maxTempBasalRate = min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasal!) let temp = predictedGlucose.recommendedTempBasal( From cf808a831bb3e3274125ded3fcd1453556e83ba6 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 19 Feb 2023 09:19:34 -0600 Subject: [PATCH 25/25] Adjust IOB clamping for temp basals to be relative to scheduled basal --- Loop/Managers/DoseMath.swift | 7 ++++ Loop/Managers/LoopDataManager.swift | 6 +-- LoopTests/Managers/LoopDataManagerTests.swift | 38 +++++++------------ 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index fb575a4385..e13a36139a 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -352,6 +352,7 @@ extension Collection where Element: GlucoseValue { /// - sensitivity: The schedule of insulin sensitivities /// - model: The insulin absorption model /// - basalRates: The schedule of basal rates + /// - additionalActiveInsulinClamp: Max amount of additional insulin above scheduled basal rate allowed to be scheduled /// - maxBasalRate: The maximum allowed basal rate /// - lastTempBasal: The previously set temp basal /// - rateRounder: Closure that rounds recommendation to nearest supported rate. If nil, no rounding is performed @@ -367,6 +368,7 @@ extension Collection where Element: GlucoseValue { model: InsulinModel, basalRates: BasalRateSchedule, maxBasalRate: Double, + additionalActiveInsulinClamp: Double? = nil, lastTempBasal: DoseEntry?, rateRounder: ((Double) -> Double)? = nil, isBasalRateScheduleOverrideActive: Bool = false, @@ -391,6 +393,11 @@ extension Collection where Element: GlucoseValue { maxBasalRate = scheduledBasalRate } + if let additionalActiveInsulinClamp { + let maxThirtyMinuteRateToKeepIOBBelowLimit = additionalActiveInsulinClamp * 2.0 + scheduledBasalRate // 30 minutes of a U/hr rate + maxBasalRate = Swift.min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasalRate) + } + let temp = correction?.asTempBasal( scheduledBasalRate: scheduledBasalRate, maxBasalRate: maxBasalRate, diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 7a7490999e..4c82d61892 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1675,9 +1675,6 @@ extension LoopDataManager { ) case .tempBasalOnly: - let maxThirtyMinuteRateToKeepIOBBelowLimit = iobHeadroom * 2.0 // 30 minutes of a U/hr rate - let maxTempBasalRate = min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasal!) - let temp = predictedGlucose.recommendedTempBasal( to: glucoseTargetRange!, at: predictedGlucose[0].startDate, @@ -1685,7 +1682,8 @@ extension LoopDataManager { sensitivity: insulinSensitivity!, model: doseStore.insulinModelProvider.model(for: pumpInsulinType), basalRates: basalRateSchedule!, - maxBasalRate: maxTempBasalRate, + maxBasalRate: maxBasal!, + additionalActiveInsulinClamp: iobHeadroom, lastTempBasal: lastTempBasal, rateRounder: rateRounder, isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index ff2f1d97b7..a11b210228 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -35,15 +35,6 @@ extension DataManagerTestType { return .tempBasalOnly } } - - var maxBolus: Double { - switch self { - case .autoBolusIOBClamping: - return 5 - default: - return LoopDataManagerDosingTests.defaultMaxBolus - } - } } extension TimeZone { @@ -77,10 +68,6 @@ class LoopDataManagerDosingTests: XCTestCase { let dateFormatter = ISO8601DateFormatter.localTimeDate() let defaultAccuracy = 1.0 / 40.0 - // MARK: Settings - let maxBasalRate = 5.0 - static let defaultMaxBolus = 10.0 - var suspendThreshold: GlucoseThreshold { return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75) } @@ -100,7 +87,7 @@ class LoopDataManagerDosingTests: XCTestCase { var automaticDosingStatus: AutomaticDosingStatus! var loopDataManager: LoopDataManager! - func setUp(for test: DataManagerTestType, basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil) { + func setUp(for test: DataManagerTestType, basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil, maxBolus: Double = 10, maxBasalRate: Double = 5.0) { let basalRateSchedule = loadBasalRateScheduleFixture("basal_profile") let settings = LoopSettings( @@ -108,7 +95,7 @@ class LoopDataManagerDosingTests: XCTestCase { glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, basalRateSchedule: basalRateSchedule, maximumBasalRatePerHour: maxBasalRate, - maximumBolus: test.maxBolus, + maximumBolus: maxBolus, suspendThreshold: suspendThreshold, automaticDosingStrategy: test.dosingStrategy ) @@ -530,8 +517,8 @@ class LoopDataManagerDosingTests: XCTestCase { let settings = LoopSettings( dosingEnabled: false, glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, - maximumBasalRatePerHour: maxBasalRate, - maximumBolus: Self.defaultMaxBolus, + maximumBasalRatePerHour: 5, + maximumBolus: 10, suspendThreshold: suspendThreshold ) @@ -585,7 +572,7 @@ class LoopDataManagerDosingTests: XCTestCase { func testAutoBolusMaxIOBClamping() { /// `maximumBolus` is set to clamp the automatic dose /// Autobolus without clamping: 0.65 U. Clamped recommendation: 0.2 U. - setUp(for: .autoBolusIOBClamping) + setUp(for: .autoBolusIOBClamping, maxBolus: 5) // This sets up dose rounding let delegate = MockDelegate() @@ -606,9 +593,9 @@ class LoopDataManagerDosingTests: XCTestCase { XCTAssertEqual(recommendedBolus!, 0.5, accuracy: 0.01) XCTAssertEqual(insulinOnBoard?.value, 9.47) - /// Set the `maximumBolus` so there's no clamping + /// Set the `maximumBolus` to 10U so there's no clamping updateGroup.enter() - self.loopDataManager.mutateSettings { settings in settings.maximumBolus = Self.defaultMaxBolus } + self.loopDataManager.mutateSettings { settings in settings.maximumBolus = 10 } self.loopDataManager.getLoopState { _, state in insulinOnBoard = state.insulinOnBoard recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits @@ -621,9 +608,9 @@ class LoopDataManagerDosingTests: XCTestCase { } func testTempBasalMaxIOBClamping() { - /// `maximumBolus` is set to clamp the automatic dose - /// Without clamping: 4.25 U/hr. Clamped recommendation: 4.2 U/hr. - setUp(for: .tempBasalIOBClamping) + /// `maximumBolus` is set to 5U to clamp max IOB at 10U + /// Without clamping: 4.25 U/hr. Clamped recommendation: 1.25 U/hr. + setUp(for: .tempBasalIOBClamping, maxBolus: 5) // This sets up dose rounding let delegate = MockDelegate() @@ -641,11 +628,12 @@ class LoopDataManagerDosingTests: XCTestCase { } updateGroup.wait() - XCTAssertEqual(recommendedBasal!.unitsPerHour, 4.2, accuracy: 0.01) + XCTAssertEqual(recommendedBasal!.unitsPerHour, 1.25, accuracy: 0.01) XCTAssertEqual(insulinOnBoard?.value, 9.87) - /// Set the `maximumBolus` so there's no clamping + /// Set the `maximumBolus` to 10U so there's no clamping updateGroup.enter() + self.loopDataManager.mutateSettings { settings in settings.maximumBolus = 10 } self.loopDataManager.getLoopState { _, state in insulinOnBoard = state.insulinOnBoard recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment