diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index ce9e1767bf..5205955465 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -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( diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 01c9bedfe5..e13a36139a 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -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) } } @@ -298,24 +298,24 @@ 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 { @@ -323,15 +323,15 @@ extension Collection where Element: GlucoseValue { } 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 @@ -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 @@ -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, @@ -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, diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 254c1f83e3..4c82d61892 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -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) @@ -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() } @@ -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 @@ -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)) } @@ -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)) @@ -1644,20 +1647,26 @@ 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, @@ -1665,14 +1674,16 @@ extension LoopDataManager { 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 @@ -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 } @@ -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)) @@ -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))", "", diff --git a/Loop/Models/LoopError.swift b/Loop/Models/LoopError.swift index babbc8bbb9..015d5cc05c 100644 --- a/Loop/Models/LoopError.swift +++ b/Loop/Models/LoopError.swift @@ -43,6 +43,7 @@ enum MissingDataErrorDetail: String, Codable { case momentumEffect case carbEffect case insulinEffect + case activeInsulin case insulinEffectIncludingPendingInsulin var localizedDetail: String { @@ -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") } @@ -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 } } diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index e22560ce8b..a11b210228 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -21,6 +21,20 @@ enum DataManagerTestType { case lowAndFallingWithCOB case lowWithLowTreatment case highAndFalling + /// uses fixtures for .highAndRisingWithCOB with a low max bolus and dosing set to autobolus + case autoBolusIOBClamping + case tempBasalIOBClamping +} + +extension DataManagerTestType { + var dosingStrategy: AutomaticDosingStrategy { + switch self { + case .autoBolusIOBClamping: + return .automaticBolus + default: + return .tempBasalOnly + } + } } extension TimeZone { @@ -54,10 +68,6 @@ class LoopDataManagerDosingTests: XCTestCase { let dateFormatter = ISO8601DateFormatter.localTimeDate() let defaultAccuracy = 1.0 / 40.0 - // MARK: Settings - let maxBasalRate = 5.0 - let maxBolus = 10.0 - var suspendThreshold: GlucoseThreshold { return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75) } @@ -77,7 +87,7 @@ class LoopDataManagerDosingTests: XCTestCase { var automaticDosingStatus: AutomaticDosingStatus! var loopDataManager: LoopDataManager! - func setUp(for test: DataManagerTestType, basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil) { + func setUp(for test: DataManagerTestType, basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil, maxBolus: Double = 10, maxBasalRate: Double = 5.0) { let basalRateSchedule = loadBasalRateScheduleFixture("basal_profile") let settings = LoopSettings( @@ -86,7 +96,8 @@ class LoopDataManagerDosingTests: XCTestCase { basalRateSchedule: basalRateSchedule, maximumBasalRatePerHour: maxBasalRate, maximumBolus: maxBolus, - suspendThreshold: suspendThreshold + suspendThreshold: suspendThreshold, + automaticDosingStrategy: test.dosingStrategy ) let doseStore = MockDoseStore(for: test) @@ -304,8 +315,8 @@ class LoopDataManagerDosingTests: XCTestCase { self.recommendation = automaticDose.recommendation completion(error) } - func roundBasalRate(unitsPerHour: Double) -> Double { unitsPerHour } - func roundBolusVolume(units: Double) -> Double { units } + func roundBasalRate(unitsPerHour: Double) -> Double { Double(Int(unitsPerHour / 0.05)) * 0.05 } + func roundBolusVolume(units: Double) -> Double { Double(Int(units / 0.05)) * 0.05 } var pumpManagerStatus: PumpManagerStatus? var cgmManagerStatus: CGMManagerStatus? var pumpStatusHighlight: DeviceStatusHighlight? @@ -432,7 +443,7 @@ class LoopDataManagerDosingTests: XCTestCase { } loopDataManager.loop() wait(for: [exp], timeout: 1.0) - let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.577747629410191, duration: .minutes(30))) + let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.55, duration: .minutes(30))) XCTAssertEqual(delegate.recommendation, expectedAutomaticDoseRecommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) if dosingDecisionStore.dosingDecisions.count == 1 { @@ -456,7 +467,7 @@ class LoopDataManagerDosingTests: XCTestCase { } loopDataManager.loop() wait(for: [exp], timeout: 1.0) - let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.577747629410191, duration: .minutes(30))) + let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.55, duration: .minutes(30))) XCTAssertNil(delegate.recommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) XCTAssertEqual(dosingDecisionStore.dosingDecisions[0].reason, "loop") @@ -506,8 +517,8 @@ class LoopDataManagerDosingTests: XCTestCase { let settings = LoopSettings( dosingEnabled: false, glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, - maximumBasalRatePerHour: maxBasalRate, - maximumBolus: maxBolus, + maximumBasalRatePerHour: 5, + maximumBolus: 10, suspendThreshold: suspendThreshold ) @@ -558,6 +569,82 @@ 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, maxBolus: 5) + + // This sets up dose rounding + let delegate = MockDelegate() + loopDataManager.delegate = delegate + + 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.5, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.47) + + /// Set the `maximumBolus` to 10U so there's no clamping + updateGroup.enter() + self.loopDataManager.mutateSettings { settings in settings.maximumBolus = 10 } + 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.47) + } + + func testTempBasalMaxIOBClamping() { + /// `maximumBolus` is set to 5U to clamp max IOB at 10U + /// Without clamping: 4.25 U/hr. Clamped recommendation: 1.25 U/hr. + setUp(for: .tempBasalIOBClamping, maxBolus: 5) + + // This sets up dose rounding + let delegate = MockDelegate() + loopDataManager.delegate = delegate + + let updateGroup = DispatchGroup() + updateGroup.enter() + + var insulinOnBoard: InsulinValue? + var recommendedBasal: TempBasalRecommendation? + self.loopDataManager.getLoopState { _, state in + insulinOnBoard = state.insulinOnBoard + recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment + updateGroup.leave() + } + updateGroup.wait() + + XCTAssertEqual(recommendedBasal!.unitsPerHour, 1.25, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.87) + + /// Set the `maximumBolus` to 10U so there's no clamping + updateGroup.enter() + self.loopDataManager.mutateSettings { settings in settings.maximumBolus = 10 } + self.loopDataManager.getLoopState { _, state in + insulinOnBoard = state.insulinOnBoard + recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment + updateGroup.leave() + } + updateGroup.wait() + + XCTAssertEqual(recommendedBasal!.unitsPerHour, 4.25, accuracy: 0.01) + XCTAssertEqual(insulinOnBoard?.value, 9.87) + } + } extension LoopDataManagerDosingTests { diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index f61fcd70ed..1d95d92a1c 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -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, .tempBasalIOBClamping: return "high_and_rising_with_cob_carb_effect" case .lowAndFallingWithCOB: return "low_and_falling_carb_effect" diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index 20d4892db0..24bcbac33c 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -60,7 +60,14 @@ class MockDoseStore: DoseStoreProtocol { } func insulinOnBoard(at date: Date, completion: @escaping (DoseStoreResult) -> Void) { - completion(.failure(.configurationError)) + switch testType { + case .highAndRisingWithCOB, .flatAndStable, .highAndFalling, .highAndStable, .lowAndFallingWithCOB, .lowWithLowTreatment: + completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.5))) + case .autoBolusIOBClamping: + completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.47))) + case .tempBasalIOBClamping: + completion(.success(.init(startDate: MockDoseStore.currentDate(for: testType), value: 9.87))) + } } func generateDiagnosticReport(_ completion: @escaping (String) -> Void) { @@ -112,7 +119,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, .tempBasalIOBClamping: return dateFormatter.date(from: "2020-08-11T21:48:17")! case .lowAndFallingWithCOB: return dateFormatter.date(from: "2020-08-11T22:06:06")! @@ -140,7 +147,7 @@ extension MockDoseStore { return "flat_and_stable_insulin_effect" case .highAndStable: return "high_and_stable_insulin_effect" - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return "high_and_rising_with_cob_insulin_effect" case .lowAndFallingWithCOB: return "low_and_falling_insulin_effect" diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index 2a5e1d2123..39adfc16bf 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -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, .tempBasalIOBClamping: return "high_and_rising_with_cob_counteraction_effect" case .lowAndFallingWithCOB: return "low_and_falling_counteraction_effect" @@ -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, .tempBasalIOBClamping: return "high_and_rising_with_cob_momentum_effect" case .lowAndFallingWithCOB: return "low_and_falling_momentum_effect" @@ -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, .tempBasalIOBClamping: return dateFormatter.date(from: "2020-08-11T21:48:17")! case .lowAndFallingWithCOB: return dateFormatter.date(from: "2020-08-11T22:06:06")! @@ -163,7 +163,7 @@ extension MockGlucoseStore { return 123.42849966275706 case .highAndStable: return 200.0 - case .highAndRisingWithCOB: + case .highAndRisingWithCOB, .autoBolusIOBClamping, .tempBasalIOBClamping: return 129.93174411197853 case .lowAndFallingWithCOB: return 75.10768374646841 diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 13f0a36c0e..23946cc16f 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -784,6 +784,8 @@ fileprivate class MockLoopState: LoopState { var carbsOnBoard: CarbValue? + var insulinOnBoard: InsulinValue? + var error: LoopError? var insulinCounteractionEffects: [GlucoseEffectVelocity] = []