Skip to content

Commit 97b7d8d

Browse files
authored
Merge pull request #1 from novalegra/max-auto-iob
Add `LoopDataManager`-level testing for auto bolus clamping
2 parents 0ef5008 + 3c7c65b commit 97b7d8d

File tree

7 files changed

+100
-33
lines changed

7 files changed

+100
-33
lines changed

Loop/Managers/DoseMath.swift

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -329,19 +329,17 @@ extension Collection where Element: GlucoseValue {
329329
minTarget: minGlucoseTargets.lowerBound,
330330
units: units
331331
)
332-
} else if eventual.quantity > eventualGlucoseTargets.upperBound,
333-
var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
334-
{
335-
// Limit automatic dosing to prevent insulinOnBoard > automaticDosingIOBLimit
336-
if let automaticDosingIOBLimit = automaticDosingIOBLimit,
337-
let insulinOnBoard = insulinOnBoard,
338-
minCorrectionUnits > 0
332+
} else if eventual.quantity > eventualGlucoseTargets.upperBound, var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose {
333+
/// Don't allow the correction units + current `insulinOnBoard` to go over `automaticDosingIOBLimit`
334+
if
335+
let automaticDosingIOBLimit,
336+
let insulinOnBoard,
337+
minCorrectionUnits > 0,
338+
insulinOnBoard + minCorrectionUnits > automaticDosingIOBLimit
339339
{
340-
let checkAutomaticDosing = automaticDosingIOBLimit - (insulinOnBoard + minCorrectionUnits)
341-
if checkAutomaticDosing < 0 {
342-
// TO DO - nice to have logging but not required
343-
minCorrectionUnits = Swift.max(minCorrectionUnits+checkAutomaticDosing, 0)
344-
}
340+
let unitsOverAutomaticDosingLimit = (insulinOnBoard + minCorrectionUnits) - automaticDosingIOBLimit
341+
// TO DO - nice to have logging but not required
342+
minCorrectionUnits = Swift.max(minCorrectionUnits - unitsOverAutomaticDosingLimit, 0)
345343
}
346344

347345
return .aboveRange(

Loop/Managers/LoopDataManager.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ final class LoopDataManager {
6363

6464
private var timeBasedDoseApplicationFactor: Double = 1.0
6565

66-
private var insulinOnBoardValue: Double?
66+
private var insulinOnBoard: InsulinValue?
6767

6868
deinit {
6969
for observer in notificationObservers {
@@ -1036,17 +1036,13 @@ extension LoopDataManager {
10361036
updateGroup.leave()
10371037
}
10381038
}
1039-
1040-
var insulinOnBoard: InsulinValue?
1041-
10421039
updateGroup.enter()
10431040
doseStore.insulinOnBoard(at: now()) { result in
10441041
switch result {
10451042
case .failure(let error):
10461043
warnings.append(.fetchDataWarning(.insulinOnBoard(error: error)))
10471044
case .success(let insulinValue):
1048-
insulinOnBoard = insulinValue
1049-
self.insulinOnBoardValue = insulinValue.value
1045+
self.insulinOnBoard = insulinValue
10501046
}
10511047
updateGroup.leave()
10521048
}
@@ -1067,7 +1063,7 @@ extension LoopDataManager {
10671063
dosingDecision.date = now()
10681064
dosingDecision.historicalGlucose = historicalGlucose
10691065
dosingDecision.carbsOnBoard = carbsOnBoard
1070-
dosingDecision.insulinOnBoard = insulinOnBoard
1066+
dosingDecision.insulinOnBoard = self.insulinOnBoard
10711067
dosingDecision.glucoseTargetRangeSchedule = settings.effectiveGlucoseTargetRangeSchedule()
10721068

10731069
// These will be updated by updatePredictedGlucoseAndRecommendedDose, if possible
@@ -1654,7 +1650,7 @@ extension LoopDataManager {
16541650
volumeRounder: volumeRounder,
16551651
rateRounder: rateRounder,
16561652
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true,
1657-
insulinOnBoard: self.insulinOnBoardValue,
1653+
insulinOnBoard: self.insulinOnBoard?.value,
16581654
automaticDosingIOBLimit: automaticDosingIOBLimit
16591655
)
16601656
case .tempBasalOnly:
@@ -1669,7 +1665,7 @@ extension LoopDataManager {
16691665
lastTempBasal: lastTempBasal,
16701666
rateRounder: rateRounder,
16711667
isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true,
1672-
insulinOnBoard: self.insulinOnBoardValue,
1668+
insulinOnBoard: self.insulinOnBoard?.value,
16731669
automaticDosingIOBLimit: automaticDosingIOBLimit
16741670
)
16751671
dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp)
@@ -1755,6 +1751,9 @@ extension LoopDataManager {
17551751
protocol LoopState {
17561752
/// The last-calculated carbs on board
17571753
var carbsOnBoard: CarbValue? { get }
1754+
1755+
/// The last-calculated insulin on board
1756+
var insulinOnBoard: InsulinValue? { get }
17581757

17591758
/// An error in the current state of the loop, or one that happened during the last attempt to loop.
17601759
var error: LoopError? { get }
@@ -1857,6 +1856,11 @@ extension LoopDataManager {
18571856
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
18581857
return loopDataManager.carbsOnBoard
18591858
}
1859+
1860+
var insulinOnBoard: InsulinValue? {
1861+
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
1862+
return loopDataManager.insulinOnBoard
1863+
}
18601864

18611865
var error: LoopError? {
18621866
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
@@ -2061,6 +2065,7 @@ extension LoopDataManager {
20612065
"lastLoopCompleted: \(String(describing: manager.lastLoopCompleted))",
20622066
"basalDeliveryState: \(String(describing: manager.basalDeliveryState))",
20632067
"carbsOnBoard: \(String(describing: state.carbsOnBoard))",
2068+
"insulinOnBoard: \(String(describing: manager.insulinOnBoard))",
20642069
"error: \(String(describing: state.error))",
20652070
"overrideInUserDefaults: \(String(describing: UserDefaults.appGroup?.intentExtensionOverrideToSet))",
20662071
"",

LoopTests/Managers/LoopDataManagerTests.swift

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ enum DataManagerTestType {
2121
case lowAndFallingWithCOB
2222
case lowWithLowTreatment
2323
case highAndFalling
24+
/// uses fixtures for .highAndRisingWithCOB with a low max bolus and dosing set to autobolus
25+
case autoBolusIOBClamping
26+
}
27+
28+
extension DataManagerTestType {
29+
var dosingStrategy: AutomaticDosingStrategy {
30+
switch self {
31+
case .autoBolusIOBClamping:
32+
return .automaticBolus
33+
default:
34+
return .tempBasalOnly
35+
}
36+
}
37+
38+
var maxBolus: Double {
39+
switch self {
40+
case .autoBolusIOBClamping:
41+
return 5
42+
default:
43+
return LoopDataManagerDosingTests.defaultMaxBolus
44+
}
45+
}
2446
}
2547

2648
extension TimeZone {
@@ -56,7 +78,7 @@ class LoopDataManagerDosingTests: XCTestCase {
5678

5779
// MARK: Settings
5880
let maxBasalRate = 5.0
59-
let maxBolus = 10.0
81+
static let defaultMaxBolus = 10.0
6082

6183
var suspendThreshold: GlucoseThreshold {
6284
return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75)
@@ -85,8 +107,9 @@ class LoopDataManagerDosingTests: XCTestCase {
85107
glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
86108
basalRateSchedule: basalRateSchedule,
87109
maximumBasalRatePerHour: maxBasalRate,
88-
maximumBolus: maxBolus,
89-
suspendThreshold: suspendThreshold
110+
maximumBolus: test.maxBolus,
111+
suspendThreshold: suspendThreshold,
112+
automaticDosingStrategy: test.dosingStrategy
90113
)
91114

92115
let doseStore = MockDoseStore(for: test)
@@ -507,7 +530,7 @@ class LoopDataManagerDosingTests: XCTestCase {
507530
dosingEnabled: false,
508531
glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
509532
maximumBasalRatePerHour: maxBasalRate,
510-
maximumBolus: maxBolus,
533+
maximumBolus: Self.defaultMaxBolus,
511534
suspendThreshold: suspendThreshold
512535
)
513536

@@ -558,6 +581,40 @@ class LoopDataManagerDosingTests: XCTestCase {
558581
XCTAssertNil(mockDelegate.recommendation)
559582
}
560583

584+
585+
func testAutoBolusMaxIOBClamping() {
586+
/// `maximumBolus` is set to clamp the automatic dose
587+
/// Autobolus without clamping: 0.65 U. Clamped recommendation: 0.2 U.
588+
setUp(for: .autoBolusIOBClamping)
589+
590+
let updateGroup = DispatchGroup()
591+
updateGroup.enter()
592+
593+
var insulinOnBoard: InsulinValue?
594+
var recommendedBolus: Double?
595+
self.loopDataManager.getLoopState { _, state in
596+
insulinOnBoard = state.insulinOnBoard
597+
recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits
598+
updateGroup.leave()
599+
}
600+
updateGroup.wait()
601+
602+
XCTAssertEqual(recommendedBolus!, 0.2, accuracy: 0.01)
603+
XCTAssertEqual(insulinOnBoard?.value, 9.5)
604+
605+
/// Set the `maximumBolus` so there's no clamping
606+
updateGroup.enter()
607+
self.loopDataManager.mutateSettings { settings in settings.maximumBolus = Self.defaultMaxBolus }
608+
self.loopDataManager.getLoopState { _, state in
609+
insulinOnBoard = state.insulinOnBoard
610+
recommendedBolus = state.recommendedAutomaticDose?.recommendation.bolusUnits
611+
updateGroup.leave()
612+
}
613+
updateGroup.wait()
614+
615+
XCTAssertEqual(recommendedBolus!, 0.65, accuracy: 0.01)
616+
XCTAssertEqual(insulinOnBoard?.value, 9.5)
617+
}
561618
}
562619

563620
extension LoopDataManagerDosingTests {

LoopTests/Mock Stores/MockCarbStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ extension MockCarbStore {
124124
return "flat_and_stable_carb_effect"
125125
case .highAndStable:
126126
return "high_and_stable_carb_effect"
127-
case .highAndRisingWithCOB:
127+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
128128
return "high_and_rising_with_cob_carb_effect"
129129
case .lowAndFallingWithCOB:
130130
return "low_and_falling_carb_effect"

LoopTests/Mock Stores/MockDoseStore.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ class MockDoseStore: DoseStoreProtocol {
6060
}
6161

6262
func insulinOnBoard(at date: Date, completion: @escaping (DoseStoreResult<InsulinValue>) -> Void) {
63-
completion(.failure(.configurationError))
63+
switch testType {
64+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
65+
completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.5)))
66+
default:
67+
completion(.failure(.configurationError))
68+
}
6469
}
6570

6671
func generateDiagnosticReport(_ completion: @escaping (String) -> Void) {
@@ -112,7 +117,7 @@ class MockDoseStore: DoseStoreProtocol {
112117
return dateFormatter.date(from: "2020-08-11T20:45:02")!
113118
case .highAndStable:
114119
return dateFormatter.date(from: "2020-08-12T12:39:22")!
115-
case .highAndRisingWithCOB:
120+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
116121
return dateFormatter.date(from: "2020-08-11T21:48:17")!
117122
case .lowAndFallingWithCOB:
118123
return dateFormatter.date(from: "2020-08-11T22:06:06")!
@@ -140,7 +145,7 @@ extension MockDoseStore {
140145
return "flat_and_stable_insulin_effect"
141146
case .highAndStable:
142147
return "high_and_stable_insulin_effect"
143-
case .highAndRisingWithCOB:
148+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
144149
return "high_and_rising_with_cob_insulin_effect"
145150
case .lowAndFallingWithCOB:
146151
return "low_and_falling_insulin_effect"

LoopTests/Mock Stores/MockGlucoseStore.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ extension MockGlucoseStore {
112112
return "flat_and_stable_counteraction_effect"
113113
case .highAndStable:
114114
return "high_and_stable_counteraction_effect"
115-
case .highAndRisingWithCOB:
115+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
116116
return "high_and_rising_with_cob_counteraction_effect"
117117
case .lowAndFallingWithCOB:
118118
return "low_and_falling_counteraction_effect"
@@ -129,7 +129,7 @@ extension MockGlucoseStore {
129129
return "flat_and_stable_momentum_effect"
130130
case .highAndStable:
131131
return "high_and_stable_momentum_effect"
132-
case .highAndRisingWithCOB:
132+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
133133
return "high_and_rising_with_cob_momentum_effect"
134134
case .lowAndFallingWithCOB:
135135
return "low_and_falling_momentum_effect"
@@ -146,7 +146,7 @@ extension MockGlucoseStore {
146146
return dateFormatter.date(from: "2020-08-11T20:45:02")!
147147
case .highAndStable:
148148
return dateFormatter.date(from: "2020-08-12T12:39:22")!
149-
case .highAndRisingWithCOB:
149+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
150150
return dateFormatter.date(from: "2020-08-11T21:48:17")!
151151
case .lowAndFallingWithCOB:
152152
return dateFormatter.date(from: "2020-08-11T22:06:06")!
@@ -163,7 +163,7 @@ extension MockGlucoseStore {
163163
return 123.42849966275706
164164
case .highAndStable:
165165
return 200.0
166-
case .highAndRisingWithCOB:
166+
case .highAndRisingWithCOB, .autoBolusIOBClamping:
167167
return 129.93174411197853
168168
case .lowAndFallingWithCOB:
169169
return 75.10768374646841

LoopTests/ViewModels/BolusEntryViewModelTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,8 @@ fileprivate class MockLoopState: LoopState {
764764

765765
var carbsOnBoard: CarbValue?
766766

767+
var insulinOnBoard: InsulinValue?
768+
767769
var error: LoopError?
768770

769771
var insulinCounteractionEffects: [GlucoseEffectVelocity] = []

0 commit comments

Comments
 (0)