Skip to content

Commit 911406c

Browse files
marionbarkernovalegraps2
authored
Use maxBolus to set automaticDosingIOBLimit (#1871)
* Use maxBolus and ratio to set maxAutoIOB * increase ratioMaxAutoInsulinOnBoardToMaxBolus to 2.0 * remove print statements * restore LoopContants * modify name from maxAutoIOB to automaticDosingIOBLimit * Code cleanup in DoseMath * configure new optional commands with default nil DoseMathTests should work without modification * remove whitespace * Add automaticIOBLimitTests * DoseMathTests: add new args to all automated dosing tests * remove defaults so new parameters are required * Modify method for providing insulinOnBoard in LoopDataManager * AlertManagerTests: add new parameter * match whitespace * `insulinOnBoardValue` -> `insulinOnBoard` for logging purposes * Add test for autobolus clamping * 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 * DoseMathTests: use non-zero value for insulinOnBoard * DoseMathTests: move insulinOnBoard internal to test functions * Move IOB limit handling into recommendedAutomaticDose, and recommendedTempBasal methods * Temp basals limited by iob max * Cleanup * Remove unintentional edit * Fix maxThirtyMinuteRateToKeepIOBBelowLimit calculation * Adjust IOB clamping for temp basals to be relative to scheduled basal --------- Co-authored-by: Anna Quinlan <anna.quinlan123@gmail.com> Co-authored-by: Pete Schwamb <pete@schwamb.net>
1 parent f6efd72 commit 911406c

9 files changed

Lines changed: 169 additions & 45 deletions

File tree

DoseMathTests/DoseMathTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ class RecommendTempBasalTests: XCTestCase {
751751

752752
func testHighAndFalling() {
753753
let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
754-
754+
755755
let insulinModel = WalshInsulinModel(actionDuration: insulinActionDuration, delay: 0)
756756

757757
let dose = glucose.recommendedTempBasal(

Loop/Managers/DoseMath.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ extension InsulinCorrection {
122122

123123
let partialDose = units * partialApplicationFactor
124124

125-
return Swift.min(Swift.max(0, volumeRounder?(partialDose) ?? partialDose),maxBolusUnits)
125+
return Swift.min(Swift.max(0, volumeRounder?(partialDose) ?? partialDose),volumeRounder?(maxBolusUnits) ?? maxBolusUnits)
126126
}
127127
}
128128

@@ -298,40 +298,40 @@ extension Collection where Element: GlucoseValue {
298298
minCorrectionUnits = correctionUnits
299299
}
300300

301-
guard let eventual = eventualGlucose, let min = minGlucose else {
301+
guard let eventualGlucose, let minGlucose else {
302302
return nil
303303
}
304304

305305
// Choose either the minimum glucose or eventual glucose as the correction delta
306-
let minGlucoseTargets = correctionRange.quantityRange(at: min.startDate)
307-
let eventualGlucoseTargets = correctionRange.quantityRange(at: eventual.startDate)
306+
let minGlucoseTargets = correctionRange.quantityRange(at: minGlucose.startDate)
307+
let eventualGlucoseTargets = correctionRange.quantityRange(at: eventualGlucose.startDate)
308308

309309
// Treat the mininum glucose when both are below range
310-
if min.quantity < minGlucoseTargets.lowerBound &&
311-
eventual.quantity < eventualGlucoseTargets.lowerBound
310+
if minGlucose.quantity < minGlucoseTargets.lowerBound &&
311+
eventualGlucose.quantity < eventualGlucoseTargets.lowerBound
312312
{
313-
let time = min.startDate.timeIntervalSince(date)
313+
let time = minGlucose.startDate.timeIntervalSince(date)
314314
// For 0 <= time <= effectDelay, assume a small amount effected. This will result in large (negative) unit recommendation rather than no recommendation at all.
315315
let percentEffected = Swift.max(.ulpOfOne, 1 - model.percentEffectRemaining(at: time))
316316

317317
guard let units = insulinCorrectionUnits(
318-
fromValue: min.quantity.doubleValue(for: unit),
318+
fromValue: minGlucose.quantity.doubleValue(for: unit),
319319
toValue: minGlucoseTargets.averageValue(for: unit),
320320
effectedSensitivity: sensitivityValue * percentEffected
321321
) else {
322322
return nil
323323
}
324324

325325
return .entirelyBelowRange(
326-
min: min,
326+
min: minGlucose,
327327
minTarget: minGlucoseTargets.lowerBound,
328328
units: units
329329
)
330-
} else if eventual.quantity > eventualGlucoseTargets.upperBound,
330+
} else if eventualGlucose.quantity > eventualGlucoseTargets.upperBound,
331331
let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
332332
{
333333
return .aboveRange(
334-
min: min,
334+
min: minGlucose,
335335
correcting: correctingGlucose,
336336
minTarget: eventualGlucoseTargets.lowerBound,
337337
units: minCorrectionUnits
@@ -352,6 +352,7 @@ extension Collection where Element: GlucoseValue {
352352
/// - sensitivity: The schedule of insulin sensitivities
353353
/// - model: The insulin absorption model
354354
/// - basalRates: The schedule of basal rates
355+
/// - additionalActiveInsulinClamp: Max amount of additional insulin above scheduled basal rate allowed to be scheduled
355356
/// - maxBasalRate: The maximum allowed basal rate
356357
/// - lastTempBasal: The previously set temp basal
357358
/// - rateRounder: Closure that rounds recommendation to nearest supported rate. If nil, no rounding is performed
@@ -367,6 +368,7 @@ extension Collection where Element: GlucoseValue {
367368
model: InsulinModel,
368369
basalRates: BasalRateSchedule,
369370
maxBasalRate: Double,
371+
additionalActiveInsulinClamp: Double? = nil,
370372
lastTempBasal: DoseEntry?,
371373
rateRounder: ((Double) -> Double)? = nil,
372374
isBasalRateScheduleOverrideActive: Bool = false,
@@ -391,6 +393,11 @@ extension Collection where Element: GlucoseValue {
391393
maxBasalRate = scheduledBasalRate
392394
}
393395

396+
if let additionalActiveInsulinClamp {
397+
let maxThirtyMinuteRateToKeepIOBBelowLimit = additionalActiveInsulinClamp * 2.0 + scheduledBasalRate // 30 minutes of a U/hr rate
398+
maxBasalRate = Swift.min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasalRate)
399+
}
400+
394401
let temp = correction?.asTempBasal(
395402
scheduledBasalRate: scheduledBasalRate,
396403
maxBasalRate: maxBasalRate,

Loop/Managers/LoopDataManager.swift

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ final class LoopDataManager {
6363

6464
private var timeBasedDoseApplicationFactor: Double = 1.0
6565

66+
private var insulinOnBoard: InsulinValue?
67+
6668
deinit {
6769
for observer in notificationObservers {
6870
NotificationCenter.default.removeObserver(observer)
@@ -1034,16 +1036,13 @@ extension LoopDataManager {
10341036
updateGroup.leave()
10351037
}
10361038
}
1037-
1038-
var insulinOnBoard: InsulinValue?
1039-
10401039
updateGroup.enter()
10411040
doseStore.insulinOnBoard(at: now()) { result in
10421041
switch result {
10431042
case .failure(let error):
10441043
warnings.append(.fetchDataWarning(.insulinOnBoard(error: error)))
10451044
case .success(let insulinValue):
1046-
insulinOnBoard = insulinValue
1045+
self.insulinOnBoard = insulinValue
10471046
}
10481047
updateGroup.leave()
10491048
}
@@ -1064,7 +1063,7 @@ extension LoopDataManager {
10641063
dosingDecision.date = now()
10651064
dosingDecision.historicalGlucose = historicalGlucose
10661065
dosingDecision.carbsOnBoard = carbsOnBoard
1067-
dosingDecision.insulinOnBoard = insulinOnBoard
1066+
dosingDecision.insulinOnBoard = self.insulinOnBoard
10681067
dosingDecision.glucoseTargetRangeSchedule = settings.effectiveGlucoseTargetRangeSchedule()
10691068

10701069
// These will be updated by updatePredictedGlucoseAndRecommendedDose, if possible
@@ -1565,8 +1564,8 @@ extension LoopDataManager {
15651564
errors.append(.configurationError(.glucoseTargetRangeSchedule))
15661565
}
15671566

1568-
let basalRates = basalRateScheduleApplyingOverrideHistory
1569-
if basalRates == nil {
1567+
let basalRateSchedule = basalRateScheduleApplyingOverrideHistory
1568+
if basalRateSchedule == nil {
15701569
errors.append(.configurationError(.basalRateSchedule))
15711570
}
15721571

@@ -1605,6 +1604,10 @@ extension LoopDataManager {
16051604
errors.append(.missingDataError(.insulinEffectIncludingPendingInsulin))
16061605
}
16071606

1607+
if self.insulinOnBoard == nil {
1608+
errors.append(.missingDataError(.activeInsulin))
1609+
}
1610+
16081611
dosingDecision.appendErrors(errors)
16091612
if let error = errors.first {
16101613
logger.error("%{public}@", String(describing: error))
@@ -1644,35 +1647,43 @@ extension LoopDataManager {
16441647

16451648
let dosingRecommendation: AutomaticDoseRecommendation?
16461649

1650+
// automaticDosingIOBLimit calculated from the user entered maxBolus
1651+
let automaticDosingIOBLimit = maxBolus! * 2.0
1652+
let iobHeadroom = automaticDosingIOBLimit - self.insulinOnBoard!.value
1653+
16471654
switch settings.automaticDosingStrategy {
16481655
case .automaticBolus:
16491656
let volumeRounder = { (_ units: Double) in
16501657
return self.delegate?.roundBolusVolume(units: units) ?? units
16511658
}
16521659

1660+
let maxAutomaticBolus = min(iobHeadroom, maxBolus! * LoopConstants.bolusPartialApplicationFactor)
1661+
16531662
dosingRecommendation = predictedGlucose.recommendedAutomaticDose(
16541663
to: glucoseTargetRange!,
16551664
at: predictedGlucose[0].startDate,
16561665
suspendThreshold: settings.suspendThreshold?.quantity,
16571666
sensitivity: insulinSensitivity!,
16581667
model: doseStore.insulinModelProvider.model(for: pumpInsulinType),
1659-
basalRates: basalRates!,
1660-
maxAutomaticBolus: maxBolus! * LoopConstants.bolusPartialApplicationFactor,
1668+
basalRates: basalRateSchedule!,
1669+
maxAutomaticBolus: maxAutomaticBolus,
16611670
partialApplicationFactor: LoopConstants.bolusPartialApplicationFactor * self.timeBasedDoseApplicationFactor,
16621671
lastTempBasal: lastTempBasal,
16631672
volumeRounder: volumeRounder,
16641673
rateRounder: rateRounder,
16651674
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
16661675
)
16671676
case .tempBasalOnly:
1677+
16681678
let temp = predictedGlucose.recommendedTempBasal(
16691679
to: glucoseTargetRange!,
16701680
at: predictedGlucose[0].startDate,
16711681
suspendThreshold: settings.suspendThreshold?.quantity,
16721682
sensitivity: insulinSensitivity!,
16731683
model: doseStore.insulinModelProvider.model(for: pumpInsulinType),
1674-
basalRates: basalRates!,
1684+
basalRates: basalRateSchedule!,
16751685
maxBasalRate: maxBasal!,
1686+
additionalActiveInsulinClamp: iobHeadroom,
16761687
lastTempBasal: lastTempBasal,
16771688
rateRounder: rateRounder,
16781689
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
@@ -1760,6 +1771,9 @@ extension LoopDataManager {
17601771
protocol LoopState {
17611772
/// The last-calculated carbs on board
17621773
var carbsOnBoard: CarbValue? { get }
1774+
1775+
/// The last-calculated insulin on board
1776+
var insulinOnBoard: InsulinValue? { get }
17631777

17641778
/// An error in the current state of the loop, or one that happened during the last attempt to loop.
17651779
var error: LoopError? { get }
@@ -1862,6 +1876,11 @@ extension LoopDataManager {
18621876
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
18631877
return loopDataManager.carbsOnBoard
18641878
}
1879+
1880+
var insulinOnBoard: InsulinValue? {
1881+
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
1882+
return loopDataManager.insulinOnBoard
1883+
}
18651884

18661885
var error: LoopError? {
18671886
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
@@ -2066,6 +2085,7 @@ extension LoopDataManager {
20662085
"lastLoopCompleted: \(String(describing: manager.lastLoopCompleted))",
20672086
"basalDeliveryState: \(String(describing: manager.basalDeliveryState))",
20682087
"carbsOnBoard: \(String(describing: state.carbsOnBoard))",
2088+
"insulinOnBoard: \(String(describing: manager.insulinOnBoard))",
20692089
"error: \(String(describing: state.error))",
20702090
"overrideInUserDefaults: \(String(describing: UserDefaults.appGroup?.intentExtensionOverrideToSet))",
20712091
"",

Loop/Models/LoopError.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ enum MissingDataErrorDetail: String, Codable {
4343
case momentumEffect
4444
case carbEffect
4545
case insulinEffect
46+
case activeInsulin
4647
case insulinEffectIncludingPendingInsulin
4748

4849
var localizedDetail: String {
@@ -55,6 +56,8 @@ enum MissingDataErrorDetail: String, Codable {
5556
return NSLocalizedString("Carb effects", comment: "Details for missing data error when carb effects are missing")
5657
case .insulinEffect:
5758
return NSLocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects are missing")
59+
case .activeInsulin:
60+
return NSLocalizedString("Active Insulin", comment: "Details for missing data error when active insulin amount is missing")
5861
case .insulinEffectIncludingPendingInsulin:
5962
return NSLocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects including pending insulin are missing")
6063
}
@@ -68,9 +71,7 @@ enum MissingDataErrorDetail: String, Codable {
6871
return nil
6972
case .carbEffect:
7073
return nil
71-
case .insulinEffect:
72-
return nil
73-
case .insulinEffectIncludingPendingInsulin:
74+
case .insulinEffect, .activeInsulin, .insulinEffectIncludingPendingInsulin:
7475
return nil
7576
}
7677
}

0 commit comments

Comments
 (0)