Skip to content

Commit d4c674f

Browse files
authored
Fix bug where insulin effects window was shortened inadvertently (tidepool-org#11)
1 parent 9d24054 commit d4c674f

4 files changed

Lines changed: 68 additions & 17 deletions

File tree

Sources/LoopAlgorithm/AbsoluteScheduleValue.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import Foundation
88

99
public struct AbsoluteScheduleValue<T>: TimelineValue {
10-
public let startDate: Date
11-
public let endDate: Date
12-
public let value: T
10+
public var startDate: Date
11+
public var endDate: Date
12+
public var value: T
1313

1414
public init(startDate: Date, endDate: Date, value: T) {
1515
self.startDate = startDate

Sources/LoopAlgorithm/LoopAlgorithm.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,11 @@ public struct LoopAlgorithm {
207207

208208
// Extend range of insulin effects to cover glucose, if needed
209209
if let glucoseStart = glucoseHistory.first?.startDate, glucoseStart < insulinEffectsInterval.start {
210-
insulinEffectsInterval.start = glucoseStart
210+
insulinEffectsInterval = insulinEffectsInterval.extendedToInclude(glucoseStart)
211211
}
212212

213213
if let glucoseEnd = glucoseHistory.last?.endDate, glucoseEnd > insulinEffectsInterval.end {
214-
insulinEffectsInterval.end = glucoseEnd
214+
insulinEffectsInterval = insulinEffectsInterval.extendedToInclude(glucoseEnd)
215215
}
216216

217217
if useMidAbsorptionISF {

Sources/LoopAlgorithmRunner/main.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ func main() {
3131
let output = LoopAlgorithm.run(input: input)
3232

3333
let encoder = JSONEncoder()
34-
encoder.outputFormatting = .prettyPrinted
34+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
35+
encoder.dateEncodingStrategy = .iso8601
3536
let jsonData = try encoder.encode(output)
3637

3738
if let jsonString = String(data: jsonData, encoding: .utf8) {

Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,6 @@ final class LoopAlgorithmTests: XCTestCase {
2323
return (input: input, recommendation: recommendation)
2424
}
2525

26-
func testSuspend() throws {
27-
28-
let (input, recommendation) = loadScenario("suspend")
29-
30-
let output = LoopAlgorithm.run(input: input)
31-
32-
XCTAssertEqual(output.recommendation, recommendation)
33-
}
34-
3526
func loadPredictedGlucoseFixture(_ name: String) -> [PredictedGlucoseValue] {
3627
let decoder = JSONDecoder()
3728
decoder.dateDecodingStrategy = .iso8601
@@ -40,7 +31,16 @@ final class LoopAlgorithmTests: XCTestCase {
4031
return try! decoder.decode([PredictedGlucoseValue].self, from: try! Data(contentsOf: url))
4132
}
4233

43-
func testCarbsWithSensitivityChange() throws {
34+
func testSuspendScenario() throws {
35+
36+
let (input, recommendation) = loadScenario("suspend")
37+
38+
let output = LoopAlgorithm.run(input: input)
39+
40+
XCTAssertEqual(output.recommendation, recommendation)
41+
}
42+
43+
func testCarbsWithSensitivityChangeScenario() throws {
4444

4545
// This test computes a dose with a future carb entry
4646
// Between the time of dose and the startTime of the carb
@@ -101,6 +101,7 @@ final class LoopAlgorithmTests: XCTestCase {
101101
XCTAssertEqual(outputA.effects.retrospectiveCorrection.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 165)
102102
XCTAssertEqual(outputB.effects.retrospectiveCorrection.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 165)
103103

104+
// These tests fail, because the momentum effect is *not* time independent yet.
104105
// Even though all the input data is the same (just shifted in time), momentum effect varies in relation to how offset
105106
// the glucose samples are from the simulation timeline (at exact 5 minute intervals from the top of the hour)
106107
// XCTAssertEqual(outputA.effects.momentum.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 0.0)
@@ -145,7 +146,7 @@ final class LoopAlgorithmTests: XCTestCase {
145146
XCTAssertEqual(basalAdjustment!.unitsPerHour, 5.83, accuracy: 0.01)
146147
}
147148

148-
func testLiveCapture() {
149+
func testLiveCaptureScenario() {
149150
let decoder = JSONDecoder()
150151
decoder.dateDecodingStrategy = .iso8601
151152

@@ -406,4 +407,53 @@ final class LoopAlgorithmTests: XCTestCase {
406407
}
407408
}
408409

410+
func testEnactingRecommendation() throws {
411+
let date = ISO8601DateFormatter().date(from: "2024-01-03T12:00:00+0000")!
412+
var input = AlgorithmInputFixture.mock(for: date)
413+
414+
let now = input.predictionStart
415+
416+
// Insulin was suspended for a while
417+
input.doses = [
418+
FixtureInsulinDose(
419+
deliveryType: .basal,
420+
startDate: now.addingTimeInterval(-.hours(2)),
421+
endDate: now.addingTimeInterval(-.minutes(5)),
422+
volume: 0)
423+
]
424+
425+
// Rising BG
426+
input.glucoseHistory = [
427+
FixtureGlucoseSample(startDate: now.addingTimeInterval(.minutes(-15)), quantity: .glucose(105)),
428+
FixtureGlucoseSample(startDate: now.addingTimeInterval(.minutes(-10)), quantity: .glucose(110)),
429+
FixtureGlucoseSample(startDate: now.addingTimeInterval(.minutes(-5)), quantity: .glucose(115)),
430+
FixtureGlucoseSample(startDate: now.addingTimeInterval(.minutes(-0)), quantity: .glucose(120)),
431+
]
432+
433+
var output = LoopAlgorithm.run(input: input)
434+
435+
let basalAdjustment = output.recommendation!.automatic!.basalAdjustment
436+
XCTAssertEqual(basalAdjustment!.unitsPerHour, 4.94, accuracy: 0.01)
437+
438+
input.doses.append(
439+
FixtureInsulinDose(
440+
deliveryType: .basal,
441+
startDate: now,
442+
endDate: now.addingTimeInterval(.minutes(30)),
443+
volume: basalAdjustment!.unitsPerHour / 2 // 4.94 U/hr = 2.47 U delivery over 30m
444+
)
445+
)
446+
447+
// 30 seconds later
448+
input.predictionStart = input.predictionStart.addingTimeInterval(30)
449+
450+
// Add more time to insulin sensitivity timeline to account for running temp basal
451+
input.sensitivity[0].endDate = input.sensitivity[0].endDate.addingTimeInterval(.hours(1))
452+
453+
// Manual recommendation does not cut off doses in progress, like a running temp basal
454+
input.recommendationType = .manualBolus
455+
456+
output = LoopAlgorithm.run(input: input)
457+
XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 105, accuracy: 0.5)
458+
}
409459
}

0 commit comments

Comments
 (0)