Skip to content

Commit 2735876

Browse files
authored
LOOP-4665: Dosing Recommendations from Stateless LoopAlgorithm (#602)
* Changes for functional algorithm recommendations * Remove limits from IRC * Simplify prediction input to only need those elements necessary for prediction * LoopAlgorithm recommendations compiling * LoopAlgorithm.generatePrediction parameters are extracted from LoopPredictionInput struct * Comparable implementation for ManualBolusRecommendation has moved to LoopKit
1 parent fe1b0f9 commit 2735876

22 files changed

+81
-126
lines changed

Loop Status Extension/StatusViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class StatusViewController: UIViewController, NCWidgetProviding {
291291
lastGlucose.quantity.doubleValue(for: unit),
292292
at: lastGlucose.startDate,
293293
unit: unit,
294-
staleGlucoseAge: LoopCoreConstants.inputDataRecencyInterval,
294+
staleGlucoseAge: LoopAlgorithm.inputDataRecencyInterval,
295295
glucoseDisplay: context.glucoseDisplay,
296296
wasUserEntered: lastGlucose.wasUserEntered,
297297
isDisplayOnly: lastGlucose.isDisplayOnly

Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ struct StatusWidgetTimelimeEntry: TimelineEntry {
5353
}
5454
let glucoseAge = date - glucoseDate
5555

56-
return glucoseAge >= LoopCoreConstants.inputDataRecencyInterval
56+
return glucoseAge >= LoopAlgorithm.inputDataRecencyInterval
5757
}
5858
}

Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class StatusWidgetTimelineProvider: TimelineProvider {
6767

6868
// Date glucose staleness changes
6969
if let lastBGTime = newEntry.currentGlucose?.startDate {
70-
let staleBgRefreshTime = lastBGTime.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval+1)
70+
let staleBgRefreshTime = lastBGTime.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval+1)
7171
datesToRefreshWidget.append(staleBgRefreshTime)
7272
}
7373

@@ -93,7 +93,7 @@ class StatusWidgetTimelineProvider: TimelineProvider {
9393

9494
var glucose: [StoredGlucoseSample] = []
9595

96-
let startDate = Date(timeIntervalSinceNow: -LoopCoreConstants.inputDataRecencyInterval)
96+
let startDate = Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)
9797

9898
group.enter()
9999
glucoseStore.getGlucoseSamples(start: startDate) { (result) in

Loop/Extensions/DeviceDataManager+DeviceStatus.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ extension DeviceDataManager {
114114
var isGlucoseValueStale: Bool {
115115
guard let latestGlucoseDataDate = glucoseStore.latestGlucose?.startDate else { return true }
116116

117-
return Date().timeIntervalSince(latestGlucoseDataDate) > LoopCoreConstants.inputDataRecencyInterval
117+
return Date().timeIntervalSince(latestGlucoseDataDate) > LoopAlgorithm.inputDataRecencyInterval
118118
}
119119
}
120120

Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift

-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ fileprivate extension StoredDosingDecision {
168168
duration: .minutes(30)),
169169
bolusUnits: 1.25)
170170
let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 0.2,
171-
pendingInsulin: 0.75,
172171
notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: date.addingTimeInterval(.minutes(30)),
173172
quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 95.0)))),
174173
date: date.addingTimeInterval(-.minutes(1)))

Loop/Managers/CGMStalenessMonitor.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ class CGMStalenessMonitor {
4343

4444
let mostRecentGlucose = samples.map { $0.date }.max()!
4545
let cgmDataAge = -mostRecentGlucose.timeIntervalSinceNow
46-
if cgmDataAge < LoopCoreConstants.inputDataRecencyInterval {
46+
if cgmDataAge < LoopAlgorithm.inputDataRecencyInterval {
4747
self.cgmDataIsStale = false
48-
self.updateCGMStalenessTimer(expiration: mostRecentGlucose.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval))
48+
self.updateCGMStalenessTimer(expiration: mostRecentGlucose.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval))
4949
} else {
5050
self.cgmDataIsStale = true
5151
}
@@ -62,14 +62,14 @@ class CGMStalenessMonitor {
6262
}
6363

6464
private func checkCGMStaleness() {
65-
delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopCoreConstants.inputDataRecencyInterval)) { (result) in
65+
delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)) { (result) in
6666
DispatchQueue.main.async {
6767
self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: result))
6868
switch result {
6969
case .success(let sample):
7070
if let sample = sample {
7171
self.cgmDataIsStale = false
72-
self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance))
72+
self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance))
7373
} else {
7474
self.cgmDataIsStale = true
7575
}

Loop/Managers/LoopDataManager.swift

+18-31
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ extension LoopDataManager {
964964
let updateGroup = DispatchGroup()
965965

966966
let historicalGlucoseStartDate = Date(timeInterval: -LoopCoreConstants.dosingDecisionHistoricalGlucoseInterval, since: now())
967-
let inputDataRecencyStartDate = Date(timeInterval: -LoopCoreConstants.inputDataRecencyInterval, since: now())
967+
let inputDataRecencyStartDate = Date(timeInterval: -LoopAlgorithm.inputDataRecencyInterval, since: now())
968968

969969
// Fetch glucose effects as far back as we want to make retroactive analysis and historical glucose for dosing decision
970970
var historicalGlucose: [HistoricalGlucoseValue]?
@@ -1227,15 +1227,15 @@ extension LoopDataManager {
12271227
let pumpStatusDate = doseStore.lastAddedPumpData
12281228
let lastGlucoseDate = glucose.startDate
12291229

1230-
guard now().timeIntervalSince(lastGlucoseDate) <= LoopCoreConstants.inputDataRecencyInterval else {
1230+
guard now().timeIntervalSince(lastGlucoseDate) <= LoopAlgorithm.inputDataRecencyInterval else {
12311231
throw LoopError.glucoseTooOld(date: glucose.startDate)
12321232
}
12331233

12341234
guard lastGlucoseDate.timeIntervalSince(now()) <= LoopCoreConstants.futureGlucoseDataInterval else {
12351235
throw LoopError.invalidFutureGlucose(date: lastGlucoseDate)
12361236
}
12371237

1238-
guard now().timeIntervalSince(pumpStatusDate) <= LoopCoreConstants.inputDataRecencyInterval else {
1238+
guard now().timeIntervalSince(pumpStatusDate) <= LoopAlgorithm.inputDataRecencyInterval else {
12391239
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
12401240
}
12411241

@@ -1487,15 +1487,15 @@ extension LoopDataManager {
14871487
let pumpStatusDate = doseStore.lastAddedPumpData
14881488
let lastGlucoseDate = glucose.startDate
14891489

1490-
guard now().timeIntervalSince(lastGlucoseDate) <= LoopCoreConstants.inputDataRecencyInterval else {
1490+
guard now().timeIntervalSince(lastGlucoseDate) <= LoopAlgorithm.inputDataRecencyInterval else {
14911491
throw LoopError.glucoseTooOld(date: glucose.startDate)
14921492
}
14931493

1494-
guard lastGlucoseDate.timeIntervalSince(now()) <= LoopCoreConstants.inputDataRecencyInterval else {
1494+
guard lastGlucoseDate.timeIntervalSince(now()) <= LoopAlgorithm.inputDataRecencyInterval else {
14951495
throw LoopError.invalidFutureGlucose(date: lastGlucoseDate)
14961496
}
14971497

1498-
guard now().timeIntervalSince(pumpStatusDate) <= LoopCoreConstants.inputDataRecencyInterval else {
1498+
guard now().timeIntervalSince(pumpStatusDate) <= LoopAlgorithm.inputDataRecencyInterval else {
14991499
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
15001500
}
15011501

@@ -1541,16 +1541,18 @@ extension LoopDataManager {
15411541

15421542
let model = doseStore.insulinModelProvider.model(for: pumpInsulinType)
15431543

1544-
return predictedGlucose.recommendedManualBolus(
1544+
var recommendation = predictedGlucose.recommendedManualBolus(
15451545
to: glucoseTargetRange,
15461546
at: now(),
15471547
suspendThreshold: settings.suspendThreshold?.quantity,
15481548
sensitivity: insulinSensitivity,
15491549
model: model,
1550-
pendingInsulin: 0, // Pending insulin is already reflected in the prediction
1551-
maxBolus: maxBolus,
1552-
volumeRounder: volumeRounder
1550+
maxBolus: maxBolus
15531551
)
1552+
1553+
// Round to pump precision
1554+
recommendation.amount = volumeRounder(recommendation.amount)
1555+
return recommendation
15541556
}
15551557

15561558
/// Generates a correction effect based on how large the discrepancy is between the current glucose and its model predicted value.
@@ -1575,37 +1577,22 @@ extension LoopDataManager {
15751577
// Get timeline of glucose discrepancies
15761578
retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta)
15771579

1578-
// Calculate retrospective correction
1579-
let insulinSensitivity = settings.insulinSensitivitySchedule!.quantity(at: glucose.startDate)
1580-
let basalRate = settings.basalRateSchedule!.value(at: glucose.startDate)
1581-
let correctionRange = settings.glucoseTargetRangeSchedule!.quantityRange(at: glucose.startDate)
1582-
15831580
retrospectiveGlucoseEffect = retrospectiveCorrection.computeEffect(
15841581
startingAt: glucose,
15851582
retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
1586-
recencyInterval: LoopCoreConstants.inputDataRecencyInterval,
1587-
insulinSensitivity: insulinSensitivity,
1588-
basalRate: basalRate,
1589-
correctionRange: correctionRange,
1583+
recencyInterval: LoopAlgorithm.inputDataRecencyInterval,
15901584
retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval
15911585
)
15921586
}
15931587

15941588
private func computeRetrospectiveGlucoseEffect(startingAt glucose: GlucoseValue, carbEffects: [GlucoseEffect]) -> [GlucoseEffect] {
15951589

1596-
let insulinSensitivity = settings.insulinSensitivitySchedule!.quantity(at: glucose.startDate)
1597-
let basalRate = settings.basalRateSchedule!.value(at: glucose.startDate)
1598-
let correctionRange = settings.glucoseTargetRangeSchedule!.quantityRange(at: glucose.startDate)
1599-
16001590
let retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta)
16011591
let retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies.combinedSums(of: LoopMath.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier)
16021592
return retrospectiveCorrection.computeEffect(
16031593
startingAt: glucose,
16041594
retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
1605-
recencyInterval: LoopCoreConstants.inputDataRecencyInterval,
1606-
insulinSensitivity: insulinSensitivity,
1607-
basalRate: basalRate,
1608-
correctionRange: correctionRange,
1595+
recencyInterval: LoopAlgorithm.inputDataRecencyInterval,
16091596
retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval
16101597
)
16111598
}
@@ -1690,17 +1677,17 @@ extension LoopDataManager {
16901677

16911678
var errors = [LoopError]()
16921679

1693-
if startDate.timeIntervalSince(glucose.startDate) > LoopCoreConstants.inputDataRecencyInterval {
1680+
if startDate.timeIntervalSince(glucose.startDate) > LoopAlgorithm.inputDataRecencyInterval {
16941681
errors.append(.glucoseTooOld(date: glucose.startDate))
16951682
}
16961683

1697-
if glucose.startDate.timeIntervalSince(startDate) > LoopCoreConstants.inputDataRecencyInterval {
1684+
if glucose.startDate.timeIntervalSince(startDate) > LoopAlgorithm.inputDataRecencyInterval {
16981685
errors.append(.invalidFutureGlucose(date: glucose.startDate))
16991686
}
17001687

17011688
let pumpStatusDate = doseStore.lastAddedPumpData
17021689

1703-
if startDate.timeIntervalSince(pumpStatusDate) > LoopCoreConstants.inputDataRecencyInterval {
1690+
if startDate.timeIntervalSince(pumpStatusDate) > LoopAlgorithm.inputDataRecencyInterval {
17041691
errors.append(.pumpDataTooOld(date: pumpStatusDate))
17051692
}
17061693

@@ -2176,7 +2163,7 @@ extension LoopDataManager {
21762163
sensitivitySchedule: sensitivitySchedule,
21772164
at: date)
21782165

2179-
dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: bolusAmount.doubleValue(for: .internationalUnit()), pendingInsulin: 0, notice: notice),
2166+
dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: bolusAmount.doubleValue(for: .internationalUnit()), notice: notice),
21802167
date: Date())
21812168

21822169
return dosingDecision

Loop/Models/ConstantApplicationFactorStrategy.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ struct ConstantApplicationFactorStrategy: ApplicationFactorStrategy {
1818
settings: LoopSettings
1919
) -> Double {
2020
// The original strategy uses a constant dosing factor.
21-
return LoopConstants.bolusPartialApplicationFactor
21+
return LoopAlgorithm.bolusPartialApplicationFactor
2222
}
2323
}

Loop/Models/LoopConstants.swift

-3
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ enum LoopConstants {
4949

5050
static let retrospectiveCorrectionEnabled = true
5151

52-
// Percentage of recommended dose to apply as bolus when using automatic bolus dosing strategy
53-
static let bolusPartialApplicationFactor = 0.4
54-
5552
/// Loop completion aging category limits
5653
static let completionFreshLimit = TimeInterval(minutes: 6)
5754
static let completionAgingLimit = TimeInterval(minutes: 16)

Loop/Models/ManualBolusRecommendation.swift

-11
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,3 @@ extension BolusRecommendationNotice: Equatable {
6262
}
6363
}
6464

65-
66-
extension ManualBolusRecommendation: Comparable {
67-
public static func ==(lhs: ManualBolusRecommendation, rhs: ManualBolusRecommendation) -> Bool {
68-
return lhs.amount == rhs.amount
69-
}
70-
71-
public static func <(lhs: ManualBolusRecommendation, rhs: ManualBolusRecommendation) -> Bool {
72-
return lhs.amount < rhs.amount
73-
}
74-
}
75-

Loop/View Controllers/StatusTableViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ final class StatusTableViewController: LoopChartsTableViewController {
610610
hudView.cgmStatusHUD.setGlucoseQuantity(glucose.quantity.doubleValue(for: unit),
611611
at: glucose.startDate,
612612
unit: unit,
613-
staleGlucoseAge: LoopCoreConstants.inputDataRecencyInterval,
613+
staleGlucoseAge: LoopAlgorithm.inputDataRecencyInterval,
614614
glucoseDisplay: self.deviceManager.glucoseDisplay(for: glucose),
615615
wasUserEntered: glucose.wasUserEntered,
616616
isDisplayOnly: glucose.isDisplayOnly)

Loop/View Models/BolusEntryViewModel.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -818,12 +818,12 @@ extension BolusEntryViewModel {
818818

819819
var isGlucoseDataStale: Bool {
820820
guard let latestGlucoseDataDate = delegate?.mostRecentGlucoseDataDate else { return true }
821-
return now().timeIntervalSince(latestGlucoseDataDate) > LoopCoreConstants.inputDataRecencyInterval
821+
return now().timeIntervalSince(latestGlucoseDataDate) > LoopAlgorithm.inputDataRecencyInterval
822822
}
823823

824824
var isPumpDataStale: Bool {
825825
guard let latestPumpDataDate = delegate?.mostRecentPumpDataDate else { return true }
826-
return now().timeIntervalSince(latestPumpDataDate) > LoopCoreConstants.inputDataRecencyInterval
826+
return now().timeIntervalSince(latestPumpDataDate) > LoopAlgorithm.inputDataRecencyInterval
827827
}
828828

829829
var isManualGlucosePromptVisible: Bool {

Loop/Views/SimpleBolusView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ struct SimpleBolusCalculatorView_Previews: PreviewProvider {
392392

393393
func computeSimpleBolusRecommendation(at date: Date, mealCarbs: HKQuantity?, manualGlucose: HKQuantity?) -> BolusDosingDecision? {
394394
var decision = BolusDosingDecision(for: .simpleBolus)
395-
decision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 3, pendingInsulin: 0),
395+
decision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 3),
396396
date: Date())
397397
return decision
398398
}

LoopCore/LoopCoreConstants.swift

-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import Foundation
1010
import LoopKit
1111

1212
public enum LoopCoreConstants {
13-
/// The amount of time since a given date that input data should be considered valid
14-
public static let inputDataRecencyInterval = TimeInterval(minutes: 15)
15-
1613
/// The amount of time in the future a glucose value should be considered valid
1714
public static let futureGlucoseDataInterval = TimeInterval(minutes: 5)
1815

LoopTests/Fixtures/live_capture/live_capture_input.json

+21-44
Original file line numberDiff line numberDiff line change
@@ -962,48 +962,25 @@
962962
"startDate" : "2023-06-23T02:37:35Z"
963963
}
964964
],
965-
"settings" : {
966-
"basal" : [
967-
{
968-
"endDate" : "2023-06-23T05:00:00Z",
969-
"startDate" : "2023-06-22T10:00:00Z",
970-
"value" : 0.45000000000000001
971-
}
972-
],
973-
"carbRatio" : [
974-
{
975-
"endDate" : "2023-06-23T07:00:00Z",
976-
"startDate" : "2023-06-22T07:00:00Z",
977-
"value" : 11
978-
}
979-
],
980-
"maximumBasalRatePerHour" : null,
981-
"maximumBolus" : null,
982-
"sensitivity" : [
983-
{
984-
"endDate" : "2023-06-23T05:00:00Z",
985-
"startDate" : "2023-06-22T10:00:00Z",
986-
"value" : 60
987-
}
988-
],
989-
"suspendThreshold" : null,
990-
"target" : [
991-
{
992-
"endDate" : "2023-06-23T07:00:00Z",
993-
"startDate" : "2023-06-22T20:25:00Z",
994-
"value" : {
995-
"maxValue" : 115,
996-
"minValue" : 100
997-
}
998-
},
999-
{
1000-
"endDate" : "2023-06-23T08:50:00Z",
1001-
"startDate" : "2023-06-23T07:00:00Z",
1002-
"value" : {
1003-
"maxValue" : 115,
1004-
"minValue" : 100
1005-
}
1006-
}
1007-
]
1008-
}
965+
"basal" : [
966+
{
967+
"endDate" : "2023-06-23T05:00:00Z",
968+
"startDate" : "2023-06-22T10:00:00Z",
969+
"value" : 0.45000000000000001
970+
}
971+
],
972+
"carbRatio" : [
973+
{
974+
"endDate" : "2023-06-23T07:00:00Z",
975+
"startDate" : "2023-06-22T07:00:00Z",
976+
"value" : 11
977+
}
978+
],
979+
"sensitivity" : [
980+
{
981+
"endDate" : "2023-06-23T05:00:00Z",
982+
"startDate" : "2023-06-22T10:00:00Z",
983+
"value" : 60
984+
}
985+
],
1009986
}

LoopTests/Managers/LoopAlgorithmTests.swift

+12-4
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,25 @@ final class LoopAlgorithmTests: XCTestCase {
4949
}
5050

5151

52-
func testLiveCaptureWithFunctionalAlgorithm() throws {
52+
func testLiveCaptureWithFunctionalAlgorithm() {
5353
// This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests,
5454
// Using the same input data, but generating the forecast using the LoopAlgorithm.generatePrediction()
5555
// function.
5656

5757
let decoder = JSONDecoder()
5858
decoder.dateDecodingStrategy = .iso8601
5959
let url = bundle.url(forResource: "live_capture_input", withExtension: "json")!
60-
let predictionInput = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url))
61-
62-
let prediction = try LoopAlgorithm.generatePrediction(input: predictionInput)
60+
let input = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url))
61+
62+
let prediction = LoopAlgorithm.generatePrediction(
63+
glucoseHistory: input.glucoseHistory,
64+
doses: input.doses,
65+
carbEntries: input.carbEntries,
66+
basal: input.basal,
67+
sensitivity: input.sensitivity,
68+
carbRatio: input.carbRatio,
69+
useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection
70+
)
6371

6472
let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose")
6573

0 commit comments

Comments
 (0)