Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use maxBolus to set automaticDosingIOBLimit #1871

Merged
merged 33 commits into from
Feb 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5a4659c
Use maxBolus and ratio to set maxAutoIOB
marionbarker Dec 15, 2022
b022c4c
Merge branch 'dev' into wip/max-auto-iob
marionbarker Dec 18, 2022
a97e007
Merge branch 'dev' into wip/max-auto-iob
marionbarker Dec 19, 2022
2afe19a
increase ratioMaxAutoInsulinOnBoardToMaxBolus to 2.0
marionbarker Dec 19, 2022
0733eaf
remove print statements
marionbarker Dec 19, 2022
90b0470
restore LoopContants
marionbarker Dec 19, 2022
74fd253
modify name from maxAutoIOB to automaticDosingIOBLimit
marionbarker Dec 19, 2022
dd4ed94
Merge branch 'dev' into wip/max-auto-iob
marionbarker Dec 29, 2022
34bf4cb
Code cleanup in DoseMath
marionbarker Dec 29, 2022
d6698c9
configure new optional commands with default nil
marionbarker Dec 30, 2022
0c182a1
remove whitespace
marionbarker Dec 30, 2022
fc654ff
Add automaticIOBLimitTests
marionbarker Dec 30, 2022
87478d8
DoseMathTests: add new args to all automated dosing tests
marionbarker Dec 31, 2022
fae59f4
remove defaults so new parameters are required
marionbarker Dec 31, 2022
659a10d
Merge branch 'dev' into wip/max-auto-iob
marionbarker Dec 31, 2022
ea948df
Modify method for providing insulinOnBoard in LoopDataManager
marionbarker Jan 2, 2023
bdbd78f
Merge branch 'dev' into wip/max-auto-iob
marionbarker Jan 2, 2023
800eba0
AlertManagerTests: add new parameter
marionbarker Jan 2, 2023
9ac85c3
Merge branch 'dev' into wip/max-auto-iob
marionbarker Jan 2, 2023
0ef5008
match whitespace
marionbarker Jan 2, 2023
c902fbe
`insulinOnBoardValue` -> `insulinOnBoard` for logging purposes
novalegra Jan 3, 2023
8699730
Add test for autobolus clamping
novalegra Jan 3, 2023
3c7c65b
Improve readability of dose clamping logic
novalegra Jan 3, 2023
97b7d8d
Merge pull request #1 from novalegra/max-auto-iob
marionbarker Jan 3, 2023
61bea9d
DoseMathTests: use non-zero value for insulinOnBoard
marionbarker Jan 4, 2023
01c775c
DoseMathTests: move insulinOnBoard internal to test functions
marionbarker Jan 5, 2023
027d5e8
Merge branch 'wip/max-auto-iob' of https://github.com/marionbarker/Lo…
ps2 Feb 10, 2023
c6323df
Move IOB limit handling into recommendedAutomaticDose, and recommende…
ps2 Feb 18, 2023
caf2709
Temp basals limited by iob max
ps2 Feb 18, 2023
374160f
Cleanup
ps2 Feb 18, 2023
e041840
Remove unintentional edit
ps2 Feb 18, 2023
6b83d2f
Fix maxThirtyMinuteRateToKeepIOBBelowLimit calculation
marionbarker Feb 18, 2023
cf808a8
Adjust IOB clamping for temp basals to be relative to scheduled basal
ps2 Feb 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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