Skip to content

Commit 93c20c5

Browse files
authored
Limit recommended temp basals to supported rates. (#5)
* Limit recommended temp basals to supported rates. * Use pumpmanager provided rounding * Fix tests, and update to PumpManager function name change * Update naming from review comments
1 parent 0a61a8a commit 93c20c5

File tree

4 files changed

+70
-18
lines changed

4 files changed

+70
-18
lines changed

DoseMathTests/DoseMathTests.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class RecommendTempBasalTests: XCTestCase {
5454

5555
fileprivate let maxBasalRate = 3.0
5656

57+
fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 }
58+
5759
func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseValue] {
5860
let fixture: [JSONDictionary] = loadFixture(resourceName)
5961
let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter()
@@ -440,6 +442,8 @@ class RecommendBolusTests: XCTestCase {
440442

441443
fileprivate let maxBolus = 10.0
442444

445+
fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 }
446+
443447
func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseValue] {
444448
let fixture: [JSONDictionary] = loadFixture(resourceName)
445449
let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter()
@@ -560,7 +564,8 @@ class RecommendBolusTests: XCTestCase {
560564
sensitivity: insulinSensitivitySchedule,
561565
model: insulinModel,
562566
pendingInsulin: 0,
563-
maxBolus: maxBolus
567+
maxBolus: maxBolus,
568+
volumeRounder: fortyIncrementsPerUnitRounder
564569
)
565570

566571
XCTAssertEqual(1.575, dose.amount)
@@ -628,7 +633,8 @@ class RecommendBolusTests: XCTestCase {
628633
sensitivity: insulinSensitivitySchedule,
629634
model: insulinModel,
630635
pendingInsulin: 0,
631-
maxBolus: maxBolus
636+
maxBolus: maxBolus,
637+
volumeRounder: fortyIncrementsPerUnitRounder
632638
)
633639

634640
XCTAssertEqual(1.4, dose.amount)
@@ -646,7 +652,8 @@ class RecommendBolusTests: XCTestCase {
646652
sensitivity: insulinSensitivitySchedule,
647653
model: insulinModel,
648654
pendingInsulin: 1,
649-
maxBolus: maxBolus
655+
maxBolus: maxBolus,
656+
volumeRounder: fortyIncrementsPerUnitRounder
650657
)
651658

652659
XCTAssertEqual(0.575, dose.amount)
@@ -740,7 +747,8 @@ class RecommendBolusTests: XCTestCase {
740747
sensitivity: insulinSensitivitySchedule,
741748
model: ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0),
742749
pendingInsulin: 0,
743-
maxBolus: maxBolus
750+
maxBolus: maxBolus,
751+
volumeRounder: fortyIncrementsPerUnitRounder
744752
)
745753

746754
XCTAssertEqual(0.275, dose.amount)

Loop/Managers/DeviceDataManager.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,22 @@ extension DeviceDataManager {
388388
}
389389

390390
extension DeviceDataManager: LoopDataManagerDelegate {
391+
func loopDataManager(_ manager: LoopDataManager, roundBasalRate unitsPerHour: Double) -> Double {
392+
guard let pumpManager = pumpManager else {
393+
return unitsPerHour
394+
}
395+
396+
return pumpManager.roundToSupportedBasalRate(unitsPerHour: unitsPerHour)
397+
}
398+
399+
func loopDataManager(_ manager: LoopDataManager, roundBolusVolume units: Double) -> Double {
400+
guard let pumpManager = pumpManager else {
401+
return units
402+
}
403+
404+
return pumpManager.roundToSupportedBolusVolume(units: units)
405+
}
406+
391407
func loopDataManager(
392408
_ manager: LoopDataManager,
393409
didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date),
@@ -413,6 +429,8 @@ extension DeviceDataManager: LoopDataManagerDelegate {
413429
}
414430
)
415431
}
432+
433+
416434
}
417435

418436

Loop/Managers/DoseMath.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ extension InsulinCorrection {
3838
/// - scheduledBasalRate: The scheduled basal rate at the time the correction is delivered
3939
/// - maxBasalRate: The maximum allowed basal rate
4040
/// - duration: The duration of the temporary basal
41-
/// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in basal delivery
41+
/// - rateRounder: The smallest fraction of a unit supported in basal delivery
4242
/// - Returns: A temp basal recommendation
4343
fileprivate func asTempBasal(
4444
scheduledBasalRate: Double,
4545
maxBasalRate: Double,
4646
duration: TimeInterval,
47-
minimumProgrammableIncrementPerUnit: Double
47+
rateRounder: ((Double) -> Double)?
4848
) -> TempBasalRecommendation {
4949
var rate = units / (duration / TimeInterval(hours: 1)) // units/hour
5050
switch self {
@@ -55,7 +55,8 @@ extension InsulinCorrection {
5555
}
5656

5757
rate = Swift.min(maxBasalRate, Swift.max(0, rate))
58-
rate = round(rate * minimumProgrammableIncrementPerUnit) / minimumProgrammableIncrementPerUnit
58+
59+
rate = rateRounder?(rate) ?? rate
5960

6061
return TempBasalRecommendation(
6162
unitsPerHour: rate,
@@ -83,16 +84,16 @@ extension InsulinCorrection {
8384
/// - Parameters:
8485
/// - pendingInsulin: The number of units expected to be delivered, but not yet reflected in the correction
8586
/// - maxBolus: The maximum allowable bolus value in units
86-
/// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in bolus delivery
87+
/// - volumeRounder: The smallest fraction of a unit supported in bolus delivery
8788
/// - Returns: A bolus recommendation
8889
fileprivate func asBolus(
8990
pendingInsulin: Double,
9091
maxBolus: Double,
91-
minimumProgrammableIncrementPerUnit: Double
92+
volumeRounder: ((Double) -> Double)?
9293
) -> BolusRecommendation {
9394
var units = self.units - pendingInsulin
9495
units = Swift.min(maxBolus, Swift.max(0, units))
95-
units = round(units * minimumProgrammableIncrementPerUnit) / minimumProgrammableIncrementPerUnit
96+
units = volumeRounder?(units) ?? units
9697

9798
return BolusRecommendation(
9899
amount: units,
@@ -340,8 +341,8 @@ extension Collection where Element == GlucoseValue {
340341
/// - basalRates: The schedule of basal rates
341342
/// - maxBasalRate: The maximum allowed basal rate
342343
/// - lastTempBasal: The previously set temp basal
344+
/// - rateRounder: Closure that rounds recommendation to nearest supported rate. If nil, no rounding is performed
343345
/// - duration: The duration of the temporary basal
344-
/// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in basal delivery
345346
/// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command
346347
/// - Returns: The recommended temporary basal rate and duration
347348
func recommendedTempBasal(
@@ -353,8 +354,8 @@ extension Collection where Element == GlucoseValue {
353354
basalRates: BasalRateSchedule,
354355
maxBasalRate: Double,
355356
lastTempBasal: DoseEntry?,
357+
rateRounder: ((Double) -> Double)? = nil,
356358
duration: TimeInterval = .minutes(30),
357-
minimumProgrammableIncrementPerUnit: Double = 40,
358359
continuationInterval: TimeInterval = .minutes(11)
359360
) -> TempBasalRecommendation? {
360361
let correction = self.insulinCorrection(
@@ -379,7 +380,7 @@ extension Collection where Element == GlucoseValue {
379380
scheduledBasalRate: scheduledBasalRate,
380381
maxBasalRate: maxBasalRate,
381382
duration: duration,
382-
minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit
383+
rateRounder: rateRounder
383384
)
384385

385386
return temp?.ifNecessary(
@@ -400,7 +401,7 @@ extension Collection where Element == GlucoseValue {
400401
/// - model: The insulin absorption model
401402
/// - pendingInsulin: The number of units expected to be delivered, but not yet reflected in the correction
402403
/// - maxBolus: The maximum bolus to return
403-
/// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in bolus delivery
404+
/// - volumeRounder: Closure that rounds recommendation to nearest supported bolus volume. If nil, no rounding is performed
404405
/// - Returns: A bolus recommendation
405406
func recommendedBolus(
406407
to correctionRange: GlucoseRangeSchedule,
@@ -410,7 +411,7 @@ extension Collection where Element == GlucoseValue {
410411
model: InsulinModel,
411412
pendingInsulin: Double,
412413
maxBolus: Double,
413-
minimumProgrammableIncrementPerUnit: Double = 40
414+
volumeRounder: ((Double) -> Double)? = nil
414415
) -> BolusRecommendation {
415416
guard let correction = self.insulinCorrection(
416417
to: correctionRange,
@@ -425,7 +426,7 @@ extension Collection where Element == GlucoseValue {
425426
var bolus = correction.asBolus(
426427
pendingInsulin: pendingInsulin,
427428
maxBolus: maxBolus,
428-
minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit
429+
volumeRounder: volumeRounder
429430
)
430431

431432
// Handle the "current BG below target" notice here

Loop/Managers/LoopDataManager.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ final class LoopDataManager {
167167
}
168168

169169
fileprivate var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)?
170+
170171
fileprivate var recommendedBolus: (recommendation: BolusRecommendation, date: Date)?
171172

172173
fileprivate var carbsOnBoard: CarbValue?
@@ -893,6 +894,10 @@ extension LoopDataManager {
893894
recommendedTempBasal = nil
894895
return
895896
}
897+
898+
let rateRounder = { (_ rate: Double) in
899+
return self.delegate?.loopDataManager(self, roundBasalRate: rate) ?? rate
900+
}
896901

897902
let tempBasal = predictedGlucose.recommendedTempBasal(
898903
to: glucoseTargetRange,
@@ -901,7 +906,8 @@ extension LoopDataManager {
901906
model: model,
902907
basalRates: basalRates,
903908
maxBasalRate: maxBasal,
904-
lastTempBasal: lastTempBasal
909+
lastTempBasal: lastTempBasal,
910+
rateRounder: rateRounder
905911
)
906912

907913
if let temp = tempBasal {
@@ -912,13 +918,18 @@ extension LoopDataManager {
912918

913919
let pendingInsulin = try self.getPendingInsulin()
914920

921+
let volumeRounder = { (_ units: Double) in
922+
return self.delegate?.loopDataManager(self, roundBolusVolume: units) ?? units
923+
}
924+
915925
let recommendation = predictedGlucose.recommendedBolus(
916926
to: glucoseTargetRange,
917927
suspendThreshold: settings.suspendThreshold?.quantity,
918928
sensitivity: insulinSensitivity,
919929
model: model,
920930
pendingInsulin: pendingInsulin,
921-
maxBolus: maxBolus
931+
maxBolus: maxBolus,
932+
volumeRounder: volumeRounder
922933
)
923934
recommendedBolus = (recommendation: recommendation, date: startDate)
924935
}
@@ -1175,6 +1186,20 @@ protocol LoopDataManagerDelegate: class {
11751186
/// - completion: A closure called once on completion
11761187
/// - result: The enacted basal
11771188
func loopDataManager(_ manager: LoopDataManager, didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), completion: @escaping (_ result: Result<DoseEntry>) -> Void) -> Void
1189+
1190+
/// Asks the delegate to round a recommended basal rate to a supported rate
1191+
///
1192+
/// - Parameters:
1193+
/// - rate: The recommended rate in U/hr
1194+
/// - Returns: a supported rate of delivery in Units/hr. The rate returned should not be larger than the passed in rate.
1195+
func loopDataManager(_ manager: LoopDataManager, roundBasalRate unitsPerHour: Double) -> Double
1196+
1197+
/// Asks the delegate to round a recommended bolus volume to a supported volume
1198+
///
1199+
/// - Parameters:
1200+
/// - units: The recommended bolus in U
1201+
/// - Returns: a supported bolus volume in U. The volume returned should not be larger than the passed in rate.
1202+
func loopDataManager(_ manager: LoopDataManager, roundBolusVolume units: Double) -> Double
11781203
}
11791204

11801205
extension DoseStore {

0 commit comments

Comments
 (0)