Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 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
386 changes: 342 additions & 44 deletions DoseMathTests/DoseMathTests.swift

Large diffs are not rendered by default.

40 changes: 31 additions & 9 deletions Loop/Managers/DoseMath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ extension Collection where Element: GlucoseValue {
at date: Date,
suspendThreshold: HKQuantity,
sensitivity: HKQuantity,
model: InsulinModel
model: InsulinModel,
insulinOnBoard: Double?,
automaticDosingIOBLimit: Double?
) -> InsulinCorrection? {
var minGlucose: GlucoseValue?
var eventualGlucose: GlucoseValue?
Expand Down Expand Up @@ -327,9 +329,19 @@ extension Collection where Element: GlucoseValue {
minTarget: minGlucoseTargets.lowerBound,
units: units
)
} else if eventual.quantity > eventualGlucoseTargets.upperBound,
let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
{
} else if eventual.quantity > eventualGlucoseTargets.upperBound, var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose {
/// Don't allow the correction units + current `insulinOnBoard` to go over `automaticDosingIOBLimit`
if
let automaticDosingIOBLimit,
let insulinOnBoard,
minCorrectionUnits > 0,
insulinOnBoard + minCorrectionUnits > automaticDosingIOBLimit
{
let unitsOverAutomaticDosingLimit = (insulinOnBoard + minCorrectionUnits) - automaticDosingIOBLimit
// TO DO - nice to have logging but not required
minCorrectionUnits = Swift.max(minCorrectionUnits - unitsOverAutomaticDosingLimit, 0)
}

return .aboveRange(
min: min,
correcting: correctingGlucose,
Expand Down Expand Up @@ -371,14 +383,18 @@ extension Collection where Element: GlucoseValue {
rateRounder: ((Double) -> Double)? = nil,
isBasalRateScheduleOverrideActive: Bool = false,
duration: TimeInterval = .minutes(30),
continuationInterval: TimeInterval = .minutes(11)
continuationInterval: TimeInterval = .minutes(11),
insulinOnBoard: Double?,
automaticDosingIOBLimit: Double?
) -> TempBasalRecommendation? {
let correction = self.insulinCorrection(
to: correctionRange,
at: date,
suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
sensitivity: sensitivity.quantity(at: date),
model: model
model: model,
insulinOnBoard: insulinOnBoard,
automaticDosingIOBLimit: automaticDosingIOBLimit
)

let scheduledBasalRate = basalRates.value(at: date)
Expand Down Expand Up @@ -439,14 +455,18 @@ extension Collection where Element: GlucoseValue {
rateRounder: ((Double) -> Double)? = nil,
isBasalRateScheduleOverrideActive: Bool = false,
duration: TimeInterval = .minutes(30),
continuationInterval: TimeInterval = .minutes(11)
continuationInterval: TimeInterval = .minutes(11),
insulinOnBoard: Double?,
automaticDosingIOBLimit: Double?
) -> AutomaticDoseRecommendation? {
guard let correction = self.insulinCorrection(
to: correctionRange,
at: date,
suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
sensitivity: sensitivity.quantity(at: date),
model: model
model: model,
insulinOnBoard: insulinOnBoard,
automaticDosingIOBLimit: automaticDosingIOBLimit
) else {
return nil
}
Expand Down Expand Up @@ -516,7 +536,9 @@ extension Collection where Element: GlucoseValue {
at: date,
suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
sensitivity: sensitivity.quantity(at: date),
model: model
model: model,
insulinOnBoard: nil,
automaticDosingIOBLimit: nil
) else {
return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin)
}
Expand Down
29 changes: 22 additions & 7 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 @@ -1629,6 +1628,9 @@ extension LoopDataManager {

let dosingRecommendation: AutomaticDoseRecommendation?

// automaticDosingIOBLimit calculated from the user entered maxBolus
let automaticDosingIOBLimit = maxBolus! * 2.0

switch settings.automaticDosingStrategy {
case .automaticBolus:
let volumeRounder = { (_ units: Double) in
Expand All @@ -1647,7 +1649,9 @@ extension LoopDataManager {
lastTempBasal: lastTempBasal,
volumeRounder: volumeRounder,
rateRounder: rateRounder,
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true,
insulinOnBoard: self.insulinOnBoard?.value,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a failure to get insulin on board, then dosing will not be limited. The insulinOnBoard function parameter here should probably be non-optional and the loop fail with an error if it can't be retrieved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I naively thought the code where self.insulinOnBoard is set:

https://github.com/marionbarker/Loop/blob/wip/max-auto-iob/Loop/Managers/LoopDataManager.swift#L1039-L1048

with the switch for result of .failure or .success handles the case where there is a failure to get insulin on board.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking more closely (and finding your earlier comment) - it does seem to be an a warning.
Changing this is beyond what I can do.

Copy link
Copy Markdown
Collaborator

@novalegra novalegra Jan 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree that it should be non-optional if a manual bolus isn't being recommended. However, this means we'll also need to retrieve IOB when recommending a manual bolus, something that doesn't currently occur, and we won't end up using that data. Alternatively, I'm hesitant to just pass a junk IOB into insulinCorrection even if it won't be used.

Would it work to add the error check (which then would halt the recommendation for automatic dosing) and keep the IOB parameter as an optional for compatibility with manual bolusing? Also open to linking the insulinOnBoard and automaticDosingIOBLimit as one optional tuple at the call site with associated non-optional parameters, since this expresses the intent of limiting or not limiting a little better
(something like dosingLimit: (insulinOnBoard: Double, automaticDosingIOBLimit: Double)?)

automaticDosingIOBLimit: automaticDosingIOBLimit
)
case .tempBasalOnly:
let temp = predictedGlucose.recommendedTempBasal(
Expand All @@ -1660,7 +1664,9 @@ extension LoopDataManager {
maxBasalRate: maxBasal!,
lastTempBasal: lastTempBasal,
rateRounder: rateRounder,
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true,
insulinOnBoard: self.insulinOnBoard?.value,
automaticDosingIOBLimit: automaticDosingIOBLimit
)
dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp)
}
Expand Down Expand Up @@ -1745,6 +1751,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 @@ -1847,6 +1856,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 @@ -2051,6 +2065,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
65 changes: 61 additions & 4 deletions LoopTests/Managers/LoopDataManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ enum DataManagerTestType {
case lowAndFallingWithCOB
case lowWithLowTreatment
case highAndFalling
/// uses fixtures for .highAndRisingWithCOB with a low max bolus and dosing set to autobolus
case autoBolusIOBClamping
}

extension DataManagerTestType {
var dosingStrategy: AutomaticDosingStrategy {
switch self {
case .autoBolusIOBClamping:
return .automaticBolus
default:
return .tempBasalOnly
}
}

var maxBolus: Double {
switch self {
case .autoBolusIOBClamping:
return 5
default:
return LoopDataManagerDosingTests.defaultMaxBolus
}
}
}

extension TimeZone {
Expand Down Expand Up @@ -56,7 +78,7 @@ class LoopDataManagerDosingTests: XCTestCase {

// MARK: Settings
let maxBasalRate = 5.0
let maxBolus = 10.0
static let defaultMaxBolus = 10.0

var suspendThreshold: GlucoseThreshold {
return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75)
Expand Down Expand Up @@ -85,8 +107,9 @@ class LoopDataManagerDosingTests: XCTestCase {
glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
basalRateSchedule: basalRateSchedule,
maximumBasalRatePerHour: maxBasalRate,
maximumBolus: maxBolus,
suspendThreshold: suspendThreshold
maximumBolus: test.maxBolus,
suspendThreshold: suspendThreshold,
automaticDosingStrategy: test.dosingStrategy
)

let doseStore = MockDoseStore(for: test)
Expand Down Expand Up @@ -507,7 +530,7 @@ class LoopDataManagerDosingTests: XCTestCase {
dosingEnabled: false,
glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
maximumBasalRatePerHour: maxBasalRate,
maximumBolus: maxBolus,
maximumBolus: Self.defaultMaxBolus,
suspendThreshold: suspendThreshold
)

Expand Down Expand Up @@ -558,6 +581,40 @@ class LoopDataManagerDosingTests: XCTestCase {
XCTAssertNil(mockDelegate.recommendation)
}


func testAutoBolusMaxIOBClamping() {
/// `maximumBolus` is set to clamp the automatic dose
/// Autobolus without clamping: 0.65 U. Clamped recommendation: 0.2 U.
setUp(for: .autoBolusIOBClamping)

let updateGroup = DispatchGroup()
updateGroup.enter()

var insulinOnBoard: InsulinValue?
var recommendedBolus: Double?
self.loopDataManager.getLoopState { _, state in
insulinOnBoard = state.insulinOnBoard
recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits
updateGroup.leave()
}
updateGroup.wait()

XCTAssertEqual(recommendedBolus!, 0.2, accuracy: 0.01)
XCTAssertEqual(insulinOnBoard?.value, 9.5)

/// Set the `maximumBolus` so there's no clamping
updateGroup.enter()
self.loopDataManager.mutateSettings { settings in settings.maximumBolus = Self.defaultMaxBolus }
self.loopDataManager.getLoopState { _, state in
insulinOnBoard = state.insulinOnBoard
recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits
updateGroup.leave()
}
updateGroup.wait()

XCTAssertEqual(recommendedBolus!, 0.65, accuracy: 0.01)
XCTAssertEqual(insulinOnBoard?.value, 9.5)
}
}

extension LoopDataManagerDosingTests {
Expand Down
2 changes: 1 addition & 1 deletion LoopTests/Mock Stores/MockCarbStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ extension MockCarbStore {
return "flat_and_stable_carb_effect"
case .highAndStable:
return "high_and_stable_carb_effect"
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return "high_and_rising_with_cob_carb_effect"
case .lowAndFallingWithCOB:
return "low_and_falling_carb_effect"
Expand Down
11 changes: 8 additions & 3 deletions LoopTests/Mock Stores/MockDoseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ class MockDoseStore: DoseStoreProtocol {
}

func insulinOnBoard(at date: Date, completion: @escaping (DoseStoreResult<InsulinValue>) -> Void) {
completion(.failure(.configurationError))
switch testType {
case .highAndRisingWithCOB, .autoBolusIOBClamping:
completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.5)))
default:
completion(.failure(.configurationError))
}
}

func generateDiagnosticReport(_ completion: @escaping (String) -> Void) {
Expand Down Expand Up @@ -112,7 +117,7 @@ class MockDoseStore: DoseStoreProtocol {
return dateFormatter.date(from: "2020-08-11T20:45:02")!
case .highAndStable:
return dateFormatter.date(from: "2020-08-12T12:39:22")!
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return dateFormatter.date(from: "2020-08-11T21:48:17")!
case .lowAndFallingWithCOB:
return dateFormatter.date(from: "2020-08-11T22:06:06")!
Expand Down Expand Up @@ -140,7 +145,7 @@ extension MockDoseStore {
return "flat_and_stable_insulin_effect"
case .highAndStable:
return "high_and_stable_insulin_effect"
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return "high_and_rising_with_cob_insulin_effect"
case .lowAndFallingWithCOB:
return "low_and_falling_insulin_effect"
Expand Down
8 changes: 4 additions & 4 deletions LoopTests/Mock Stores/MockGlucoseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ extension MockGlucoseStore {
return "flat_and_stable_counteraction_effect"
case .highAndStable:
return "high_and_stable_counteraction_effect"
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return "high_and_rising_with_cob_counteraction_effect"
case .lowAndFallingWithCOB:
return "low_and_falling_counteraction_effect"
Expand All @@ -129,7 +129,7 @@ extension MockGlucoseStore {
return "flat_and_stable_momentum_effect"
case .highAndStable:
return "high_and_stable_momentum_effect"
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return "high_and_rising_with_cob_momentum_effect"
case .lowAndFallingWithCOB:
return "low_and_falling_momentum_effect"
Expand All @@ -146,7 +146,7 @@ extension MockGlucoseStore {
return dateFormatter.date(from: "2020-08-11T20:45:02")!
case .highAndStable:
return dateFormatter.date(from: "2020-08-12T12:39:22")!
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return dateFormatter.date(from: "2020-08-11T21:48:17")!
case .lowAndFallingWithCOB:
return dateFormatter.date(from: "2020-08-11T22:06:06")!
Expand All @@ -163,7 +163,7 @@ extension MockGlucoseStore {
return 123.42849966275706
case .highAndStable:
return 200.0
case .highAndRisingWithCOB:
case .highAndRisingWithCOB, .autoBolusIOBClamping:
return 129.93174411197853
case .lowAndFallingWithCOB:
return 75.10768374646841
Expand Down
2 changes: 2 additions & 0 deletions LoopTests/ViewModels/BolusEntryViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,8 @@ fileprivate class MockLoopState: LoopState {

var carbsOnBoard: CarbValue?

var insulinOnBoard: InsulinValue?

var error: LoopError?

var insulinCounteractionEffects: [GlucoseEffectVelocity] = []
Expand Down