Skip to content

Commit

Permalink
Use maxBolus to set automaticDosingIOBLimit (#1871)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Pete Schwamb <[email protected]>
  • Loading branch information
3 people authored Feb 19, 2023
1 parent f6efd72 commit 911406c
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 45 deletions.
2 changes: 1 addition & 1 deletion DoseMathTests/DoseMathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ class RecommendTempBasalTests: XCTestCase {

func testHighAndFalling() {
let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")

let insulinModel = WalshInsulinModel(actionDuration: insulinActionDuration, delay: 0)

let dose = glucose.recommendedTempBasal(
Expand Down
29 changes: 18 additions & 11 deletions Loop/Managers/DoseMath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -298,40 +298,40 @@ 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 {
return nil
}

return .entirelyBelowRange(
min: min,
min: minGlucose,
minTarget: minGlucoseTargets.lowerBound,
units: units
)
} else if eventual.quantity > eventualGlucoseTargets.upperBound,
} 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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
40 changes: 30 additions & 10 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ final class LoopDataManager {

private var timeBasedDoseApplicationFactor: Double = 1.0

private var insulinOnBoard: InsulinValue?

deinit {
for observer in notificationObservers {
NotificationCenter.default.removeObserver(observer)
Expand Down Expand Up @@ -1034,16 +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.insulinOnBoard = insulinValue
}
updateGroup.leave()
}
Expand All @@ -1064,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
Expand Down Expand Up @@ -1565,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))
}

Expand Down Expand Up @@ -1605,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))
Expand Down Expand Up @@ -1644,35 +1647,43 @@ extension LoopDataManager {

let dosingRecommendation: AutomaticDoseRecommendation?

// automaticDosingIOBLimit calculated from the user entered maxBolus
let automaticDosingIOBLimit = maxBolus! * 2.0
let iobHeadroom = automaticDosingIOBLimit - self.insulinOnBoard!.value

switch settings.automaticDosingStrategy {
case .automaticBolus:
let volumeRounder = { (_ units: Double) in
return self.delegate?.roundBolusVolume(units: units) ?? units
}

let maxAutomaticBolus = min(iobHeadroom, maxBolus! * LoopConstants.bolusPartialApplicationFactor)

dosingRecommendation = predictedGlucose.recommendedAutomaticDose(
to: glucoseTargetRange!,
at: predictedGlucose[0].startDate,
suspendThreshold: settings.suspendThreshold?.quantity,
sensitivity: insulinSensitivity!,
model: doseStore.insulinModelProvider.model(for: pumpInsulinType),
basalRates: basalRates!,
maxAutomaticBolus: maxBolus! * LoopConstants.bolusPartialApplicationFactor,
basalRates: basalRateSchedule!,
maxAutomaticBolus: maxAutomaticBolus,
partialApplicationFactor: LoopConstants.bolusPartialApplicationFactor * self.timeBasedDoseApplicationFactor,
lastTempBasal: lastTempBasal,
volumeRounder: volumeRounder,
rateRounder: rateRounder,
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
)
case .tempBasalOnly:

let temp = predictedGlucose.recommendedTempBasal(
to: glucoseTargetRange!,
at: predictedGlucose[0].startDate,
suspendThreshold: settings.suspendThreshold?.quantity,
sensitivity: insulinSensitivity!,
model: doseStore.insulinModelProvider.model(for: pumpInsulinType),
basalRates: basalRates!,
basalRates: basalRateSchedule!,
maxBasalRate: maxBasal!,
additionalActiveInsulinClamp: iobHeadroom,
lastTempBasal: lastTempBasal,
rateRounder: rateRounder,
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
Expand Down Expand Up @@ -1760,6 +1771,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 }
Expand Down Expand Up @@ -1862,6 +1876,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))
Expand Down Expand Up @@ -2066,6 +2085,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))",
"",
Expand Down
7 changes: 4 additions & 3 deletions Loop/Models/LoopError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum MissingDataErrorDetail: String, Codable {
case momentumEffect
case carbEffect
case insulinEffect
case activeInsulin
case insulinEffectIncludingPendingInsulin

var localizedDetail: String {
Expand All @@ -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")
}
Expand All @@ -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
}
}
Expand Down
Loading

0 comments on commit 911406c

Please sign in to comment.