diff --git a/Sources/LoopAlgorithm/Insulin/DoseMath.swift b/Sources/LoopAlgorithm/Insulin/DoseMath.swift index b9178fb..010fad9 100644 --- a/Sources/LoopAlgorithm/Insulin/DoseMath.swift +++ b/Sources/LoopAlgorithm/Insulin/DoseMath.swift @@ -193,10 +193,6 @@ extension Array where Element: GlucoseValue { let endOfAbsorption = date.addingTimeInterval(model.effectDuration) - guard let correctionRangeItem = correctionRange.closestPrior(to: date) else { - preconditionFailure("Correction range must cover date: \(date)") - } - // For each prediction above target, determine the amount of insulin necessary to correct glucose based on the modeled effectiveness of the insulin at that time for prediction in self { guard prediction.startDate >= date else { @@ -213,6 +209,13 @@ extension Array where Element: GlucoseValue { let predictedGlucoseValue = prediction.quantity.doubleValue(for: unit) let time = prediction.startDate.timeIntervalSince(date) + // The correction range in effect at this prediction point's time, so a + // forecast that crosses a target-band boundary is corrected to the band + // in effect at each point. + guard let correctionRangeItem = correctionRange.closestPrior(to: prediction.startDate) else { + preconditionFailure("Correction range must cover prediction: \(prediction.startDate)") + } + // Compute the target value as a function of time since the dose started let targetValue = targetGlucoseValue( percentEffectDuration: time / model.effectDuration, diff --git a/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift b/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift index fa9e696..a1f8c3a 100644 --- a/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift +++ b/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift @@ -570,4 +570,66 @@ class CorrectionDosingTests: XCTestCase { XCTFail("Wrong .notice: \(String(describing: manualDose.notice))") } } + + func testCorrectionRangeSampledPerPredictionPoint() { + // A flat high prediction crossing a target-band boundary two hours out. + let prediction = [ + PredictedGlucoseValue(startDate: testDate.addingTimeInterval(.hours(0)), quantity: .glucose(160)), + PredictedGlucoseValue(startDate: testDate.addingTimeInterval(.hours(6.2)), quantity: .glucose(160)) + ] + + // Target switches from 90-120 to 150-170 at t+2h; predicted points beyond + // the boundary are already in range, so they should require no correction. + let steppedTarget: GlucoseRangeTimeline = [ + AbsoluteScheduleValue( + startDate: testDate.addingTimeInterval(.hours(-24)), + endDate: testDate.addingTimeInterval(.hours(2)), + value: LoopQuantity.glucose(90)...LoopQuantity.glucose(120) + ), + AbsoluteScheduleValue( + startDate: testDate.addingTimeInterval(.hours(2)), + endDate: testDate.addingTimeInterval(.hours(24)), + value: LoopQuantity.glucose(150)...LoopQuantity.glucose(170) + ) + ] + + let uniformCorrection = LoopAlgorithm.insulinCorrection( + prediction: prediction, + at: testDate, + target: target, + suspendThreshold: suspendThreshold, + sensitivity: sensitivity, + insulinModel: insulinModel + ) + + let steppedCorrection = LoopAlgorithm.insulinCorrection( + prediction: prediction, + at: testDate, + target: steppedTarget, + suspendThreshold: suspendThreshold, + sensitivity: sensitivity, + insulinModel: insulinModel + ) + + let uniformDose = LoopAlgorithm.recommendManualBolus( + for: uniformCorrection, + maxBolus: 6, + currentGlucose: FixtureGlucoseSample(startDate: testDate, quantity: .glucose(160)), + target: target + ) + + let steppedDose = LoopAlgorithm.recommendManualBolus( + for: steppedCorrection, + maxBolus: 6, + currentGlucose: FixtureGlucoseSample(startDate: testDate, quantity: .glucose(160)), + target: steppedTarget + ) + + // With a uniform 90-120 target the whole forecast is above range. + XCTAssertGreaterThan(uniformDose.amount, 0.5) + // The points beyond t+2h sit inside the 150-170 band in effect there, so + // correcting to the per-point band requires less insulin than correcting + // the whole forecast to 90-120. + XCTAssertLessThan(steppedDose.amount, uniformDose.amount) + } }