From 4c7a759c5d3d1d557c85ddec79dcf15d0cd07735 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 18 Sep 2017 10:43:48 -0500 Subject: [PATCH] Ensure that bg prediction extends to full DIA; fixes issues with recommendations when there are no recent insulin doses --- DoseMathTests/DoseMathTests.swift | 16 +++++++ ...d_temp_start_low_end_just_above_range.json | 43 +++++++++++++++++++ Loop.xcodeproj/project.pbxproj | 10 +++-- Loop/Managers/DoseMath.swift | 3 +- Loop/Managers/LoopDataManager.swift | 15 ++++++- 5 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 DoseMathTests/Fixtures/recommended_temp_start_low_end_just_above_range.json diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 0273dbb4e3..9123bd5cbd 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -685,6 +685,22 @@ class RecommendBolusTests: XCTestCase { XCTAssertEqualWithAccuracy(0, dose.amount, accuracy: 1e-13) } + func testStartLowEndJustAboveRange() { + let glucose = loadGlucoseValueFixture("recommended_temp_start_low_end_just_above_range") + + let dose = glucose.recommendedBolus( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter(), doubleValue: 0), + sensitivity: insulinSensitivitySchedule, + model: ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0), + pendingInsulin: 0, + maxBolus: maxBolus + ) + + XCTAssertEqual(0.275, dose.amount) + } + func testHighAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") diff --git a/DoseMathTests/Fixtures/recommended_temp_start_low_end_just_above_range.json b/DoseMathTests/Fixtures/recommended_temp_start_low_end_just_above_range.json new file mode 100644 index 0000000000..919ac8dc8c --- /dev/null +++ b/DoseMathTests/Fixtures/recommended_temp_start_low_end_just_above_range.json @@ -0,0 +1,43 @@ +[ + {"date": "2017-09-17T10:38:21", "amount": 57}, + {"date": "2017-09-17T10:40:00", "amount": 57.6448}, + {"date": "2017-09-17T10:45:00", "amount": 59.7488}, + {"date": "2017-09-17T10:50:00", "amount": 61.8207}, + {"date": "2017-09-17T10:55:00", "amount": 63.8623}, + {"date": "2017-09-17T11:00:00", "amount": 65.8754}, + {"date": "2017-09-17T11:05:00", "amount": 67.8615}, + {"date": "2017-09-17T11:10:00", "amount": 69.8222}, + {"date": "2017-09-17T11:15:00", "amount": 71.759}, + {"date": "2017-09-17T11:20:00", "amount": 73.6728}, + {"date": "2017-09-17T11:25:00", "amount": 75.5648}, + {"date": "2017-09-17T11:30:00", "amount": 77.436}, + {"date": "2017-09-17T11:35:00", "amount": 79.2873}, + {"date": "2017-09-17T11:40:00", "amount": 81.1198}, + {"date": "2017-09-17T11:45:00", "amount": 82.9344}, + {"date": "2017-09-17T11:50:00", "amount": 84.7321}, + {"date": "2017-09-17T11:55:00", "amount": 86.5139}, + {"date": "2017-09-17T12:00:00", "amount": 88.281}, + {"date": "2017-09-17T12:05:00", "amount": 90.0348}, + {"date": "2017-09-17T12:10:00", "amount": 91.7764}, + {"date": "2017-09-17T12:15:00", "amount": 93.507}, + {"date": "2017-09-17T12:20:00", "amount": 95.2275}, + {"date": "2017-09-17T12:25:00", "amount": 96.9392}, + {"date": "2017-09-17T12:30:00", "amount": 98.6428}, + {"date": "2017-09-17T12:35:00", "amount": 100.339}, + {"date": "2017-09-17T12:40:00", "amount": 102.03}, + {"date": "2017-09-17T12:45:00", "amount": 103.715}, + {"date": "2017-09-17T12:50:00", "amount": 105.395}, + {"date": "2017-09-17T12:55:00", "amount": 107.072}, + {"date": "2017-09-17T13:00:00", "amount": 108.746}, + {"date": "2017-09-17T13:05:00", "amount": 110.417}, + {"date": "2017-09-17T13:10:00", "amount": 112.086}, + {"date": "2017-09-17T13:15:00", "amount": 113.753}, + {"date": "2017-09-17T13:20:00", "amount": 115.42}, + {"date": "2017-09-17T13:25:00", "amount": 117.087}, + {"date": "2017-09-17T13:30:00", "amount": 118.754}, + {"date": "2017-09-17T13:35:00", "amount": 120.42}, + {"date": "2017-09-17T13:40:00", "amount": 121.914}, + {"date": "2017-09-17T13:45:00", "amount": 121.914}, + {"date": "2017-09-17T13:50:00", "amount": 121.914}, + {"date": "2017-09-17T16:38:21", "amount": 121.914} +] diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 822995d5b0..c2d4ca196e 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 435CB6291F37B01300C320C7 /* InsulinModelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435CB6281F37B01300C320C7 /* InsulinModelSettings.swift */; }; 436961911F19D11E00447E89 /* ChartPointsContextFillLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4369618F1F19C86400447E89 /* ChartPointsContextFillLayer.swift */; }; 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; }; + 436D9BF81F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json in Resources */ = {isa = PBXBuildFile; fileRef = 436D9BF71F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json */; }; 436FACEE1D0BA636004E2427 /* InsulinDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */; }; 437272DF1F09E41200A3DA02 /* WCSession+Swift4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */; }; 437272E01F09E41600A3DA02 /* WCSession+Swift4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */; }; @@ -428,6 +429,7 @@ 43649A621C7A347F00523D7F /* CollectionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionType.swift; sourceTree = ""; }; 4369618F1F19C86400447E89 /* ChartPointsContextFillLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointsContextFillLayer.swift; sourceTree = ""; }; 436A0DA41D236A2A00104B24 /* LoopError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopError.swift; sourceTree = ""; }; + 436D9BF71F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommended_temp_start_low_end_just_above_range.json; sourceTree = ""; }; 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDataSource.swift; sourceTree = ""; }; 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WCSession+Swift4.swift"; sourceTree = ""; }; 43776F8C1B8022E90074EA36 /* Loop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Loop.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -858,8 +860,10 @@ 43E2D8E01D20C0CB004DA55F /* Fixtures */ = { isa = PBXGroup; children = ( + C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */, 43E2D8E11D20C0DB004DA55F /* read_selected_basal_profile.json */, 43E2D8E21D20C0DB004DA55F /* recommend_temp_basal_correct_low_at_min.json */, + C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */, 43E2D8E31D20C0DB004DA55F /* recommend_temp_basal_flat_and_high.json */, 43E2D8E41D20C0DB004DA55F /* recommend_temp_basal_high_and_falling.json */, 43E2D8E51D20C0DB004DA55F /* recommend_temp_basal_high_and_rising.json */, @@ -869,10 +873,9 @@ 43E2D8E91D20C0DB004DA55F /* recommend_temp_basal_start_high_end_low.json */, 43E2D8EA1D20C0DB004DA55F /* recommend_temp_basal_start_low_end_high.json */, 43E2D8EB1D20C0DB004DA55F /* recommend_temp_basal_start_low_end_in_range.json */, - C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */, C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */, - C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */, - C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */, + C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */, + 436D9BF71F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json */, ); path = Fixtures; sourceTree = ""; @@ -1420,6 +1423,7 @@ C17824A31E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json in Resources */, 43E2D8F41D20C0DB004DA55F /* recommend_temp_basal_start_high_end_low.json in Resources */, 43E2D8EF1D20C0DB004DA55F /* recommend_temp_basal_high_and_falling.json in Resources */, + 436D9BF81F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json in Resources */, 43E2D8ED1D20C0DB004DA55F /* recommend_temp_basal_correct_low_at_min.json in Resources */, 43E2D8F01D20C0DB004DA55F /* recommend_temp_basal_high_and_rising.json in Resources */, C12F21A71DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json in Resources */, diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index e34380e93a..10c0872e8f 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -264,7 +264,8 @@ extension Collection where Iterator.Element == GlucoseValue { let targetValue = targetGlucoseValue( percentEffectDuration: time / model.effectDuration, minValue: suspendThresholdValue, - maxValue: correctionRange.value(at: prediction.startDate).averageValue) + maxValue: correctionRange.value(at: prediction.startDate).averageValue + ) // Compute the dose required to bring this prediction to target: // dose = (Glucose Δ) / (% effect × sensitivity) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 1eca48103a..fcf0f16ac9 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -638,6 +638,10 @@ final class LoopDataManager { fileprivate func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + guard let model = insulinModelSettings?.model else { + throw LoopError.configurationError("Check settings") + } + guard let glucose = self.glucoseStore.latestGlucose else { throw LoopError.missingDataError(details: "Cannot predict glucose due to missing input data", recovery: "Check your CGM data source") } @@ -661,7 +665,16 @@ final class LoopDataManager { effects.append(self.retrospectiveGlucoseEffect) } - return LoopMath.predictGlucose(glucose, momentum: momentum, effects: effects) + var prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: effects) + + // Dosing requires prediction entries at as long as the insulin model duration. + // If our prediciton is shorter than that, then extend it here. + let finalDate = glucose.startDate.addingTimeInterval(model.effectDuration) + if let last = prediction.last, last.startDate < finalDate { + prediction.append(PredictedGlucoseValue(startDate: finalDate, quantity: last.quantity)) + } + + return prediction } // MARK: - Calculation state