|
|
@@ -0,0 +1,470 @@
|
|
|
+import Foundation
|
|
|
+import Testing
|
|
|
+@testable import Trio
|
|
|
+
|
|
|
+@Suite("DetermineBasal early exits before core dosing logic") struct DetermineBasalEarlyExitTests {
|
|
|
+ private func createDefaultInputs() -> (
|
|
|
+ profile: Profile,
|
|
|
+ preferences: Preferences,
|
|
|
+ currentTemp: TempBasal,
|
|
|
+ iobData: [IobResult],
|
|
|
+ mealData: ComputedCarbs,
|
|
|
+ autosensData: Autosens,
|
|
|
+ reservoirData: Decimal,
|
|
|
+ glucoseStatus: GlucoseStatus,
|
|
|
+ trioCustomOrefVariables: TrioCustomOrefVariables,
|
|
|
+ currentTime: Date
|
|
|
+ ) {
|
|
|
+ let currentTime = Date()
|
|
|
+ var profile = Profile()
|
|
|
+ profile.maxIob = 2.5
|
|
|
+ profile.dia = 3
|
|
|
+ profile.currentBasal = 0.9
|
|
|
+ profile.maxDailyBasal = 1.3
|
|
|
+ profile.maxBasal = 3.5
|
|
|
+ profile.maxBg = 120
|
|
|
+ profile.minBg = 110
|
|
|
+ profile.sens = 40
|
|
|
+ profile.carbRatio = 10
|
|
|
+ profile.thresholdSetting = 80
|
|
|
+ profile.temptargetSet = false
|
|
|
+ profile.bolusIncrement = 0.1
|
|
|
+ profile.useCustomPeakTime = false
|
|
|
+ profile.curve = .rapidActing
|
|
|
+
|
|
|
+ var preferences = Preferences()
|
|
|
+ preferences.useNewFormula = false
|
|
|
+ preferences.sigmoid = false
|
|
|
+ preferences.adjustmentFactor = 0.8
|
|
|
+ preferences.adjustmentFactorSigmoid = 0.5
|
|
|
+ preferences.curve = .rapidActing
|
|
|
+ preferences.useCustomPeakTime = false
|
|
|
+
|
|
|
+ let currentTemp = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: currentTime)
|
|
|
+
|
|
|
+ let iobData = [IobResult(
|
|
|
+ iob: 0,
|
|
|
+ activity: 0,
|
|
|
+ basaliob: 0,
|
|
|
+ bolusiob: 0,
|
|
|
+ netbasalinsulin: 0,
|
|
|
+ bolusinsulin: 0,
|
|
|
+ time: currentTime,
|
|
|
+ iobWithZeroTemp: IobResult.IobWithZeroTemp(
|
|
|
+ iob: 0,
|
|
|
+ activity: 0,
|
|
|
+ basaliob: 0,
|
|
|
+ bolusiob: 0,
|
|
|
+ netbasalinsulin: 0,
|
|
|
+ bolusinsulin: 0,
|
|
|
+ time: currentTime
|
|
|
+ ),
|
|
|
+ lastBolusTime: nil,
|
|
|
+ lastTemp: IobResult.LastTemp(
|
|
|
+ rate: 0,
|
|
|
+ timestamp: currentTime,
|
|
|
+ started_at: currentTime,
|
|
|
+ date: UInt64(currentTime.timeIntervalSince1970 * 1000),
|
|
|
+ duration: 30
|
|
|
+ )
|
|
|
+ )]
|
|
|
+
|
|
|
+ let mealData = ComputedCarbs(
|
|
|
+ carbs: 0,
|
|
|
+ mealCOB: 0,
|
|
|
+ currentDeviation: 0,
|
|
|
+ maxDeviation: 0,
|
|
|
+ minDeviation: 0,
|
|
|
+ slopeFromMaxDeviation: 0,
|
|
|
+ slopeFromMinDeviation: 0,
|
|
|
+ allDeviations: [0, 0, 0, 0, 0],
|
|
|
+ lastCarbTime: 0
|
|
|
+ )
|
|
|
+
|
|
|
+ let autosensData = Autosens(ratio: 1.0, newisf: nil)
|
|
|
+
|
|
|
+ let glucoseStatus = GlucoseStatus(
|
|
|
+ delta: 0,
|
|
|
+ glucose: 115,
|
|
|
+ noise: 1,
|
|
|
+ shortAvgDelta: 0,
|
|
|
+ longAvgDelta: 0.1,
|
|
|
+ date: currentTime,
|
|
|
+ lastCalIndex: nil,
|
|
|
+ device: "test"
|
|
|
+ )
|
|
|
+
|
|
|
+ let trioCustomOrefVariables = TrioCustomOrefVariables(
|
|
|
+ average_total_data: 0,
|
|
|
+ weightedAverage: 0,
|
|
|
+ currentTDD: 0,
|
|
|
+ past2hoursAverage: 0,
|
|
|
+ date: currentTime,
|
|
|
+ overridePercentage: 100,
|
|
|
+ useOverride: false,
|
|
|
+ duration: 0,
|
|
|
+ unlimited: false,
|
|
|
+ overrideTarget: 0,
|
|
|
+ smbIsOff: false,
|
|
|
+ advancedSettings: false,
|
|
|
+ isfAndCr: false,
|
|
|
+ isf: false,
|
|
|
+ cr: false,
|
|
|
+ smbIsScheduledOff: false,
|
|
|
+ start: 0,
|
|
|
+ end: 0,
|
|
|
+ smbMinutes: 30,
|
|
|
+ uamMinutes: 30,
|
|
|
+ shouldProtectDueToHIGH: false
|
|
|
+ )
|
|
|
+
|
|
|
+ return (
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: 100,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 1 from JS
|
|
|
+ @Test("should fail if current_basal is missing") func missingCurrentBasal() throws {
|
|
|
+ var (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ currentTemp,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ glucoseStatus,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ profile.currentBasal = nil
|
|
|
+ profile.basalprofile = [] // ensure basalFor also returns nil
|
|
|
+
|
|
|
+ #expect(throws: DeterminationError.missingCurrentBasal) {
|
|
|
+ _ = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 2 from JS
|
|
|
+ @Test("should cancel high temp if BG is 38") func cancelHighTempBG38() throws {
|
|
|
+ let (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ _,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ _,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ let glucoseStatus = GlucoseStatus(
|
|
|
+ delta: 0,
|
|
|
+ glucose: 38,
|
|
|
+ noise: 1,
|
|
|
+ shortAvgDelta: 0,
|
|
|
+ longAvgDelta: 0.1,
|
|
|
+ date: currentTime,
|
|
|
+ lastCalIndex: nil,
|
|
|
+ device: "test"
|
|
|
+ )
|
|
|
+
|
|
|
+ let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
|
|
|
+
|
|
|
+ let result = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+
|
|
|
+ #expect(result?.rate == 0)
|
|
|
+ #expect(result?.duration == 0)
|
|
|
+ #expect(result?.reason.contains("Canceling high temp basal") == true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 3 from JS
|
|
|
+ @Test("should shorten long zero temp if BG data is too old") func shortenLongZeroTempTooOldBG() throws {
|
|
|
+ let (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ _,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ _,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
|
|
|
+ let glucoseStatus = GlucoseStatus(
|
|
|
+ delta: 0,
|
|
|
+ glucose: 115,
|
|
|
+ noise: 1,
|
|
|
+ shortAvgDelta: 0,
|
|
|
+ longAvgDelta: 0.1,
|
|
|
+ date: glucoseTime,
|
|
|
+ lastCalIndex: nil,
|
|
|
+ device: "test"
|
|
|
+ )
|
|
|
+
|
|
|
+ let currentTemp = TempBasal(duration: 60, rate: 0, temp: .absolute, timestamp: currentTime)
|
|
|
+
|
|
|
+ let result = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+
|
|
|
+ #expect(result?.rate == 0)
|
|
|
+ #expect(result?.duration == 30)
|
|
|
+ #expect(result?.reason.contains("Shortening") == true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 4 from JS
|
|
|
+ @Test("should do nothing if BG is too old and temp is not high") func doNothingOldBGNotHighTemp() throws {
|
|
|
+ let (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ _,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ _,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
|
|
|
+ let glucoseStatus = GlucoseStatus(
|
|
|
+ delta: 0,
|
|
|
+ glucose: 115,
|
|
|
+ noise: 1,
|
|
|
+ shortAvgDelta: 0,
|
|
|
+ longAvgDelta: 0.1,
|
|
|
+ date: glucoseTime,
|
|
|
+ lastCalIndex: nil,
|
|
|
+ device: "test"
|
|
|
+ )
|
|
|
+
|
|
|
+ let currentTemp = TempBasal(duration: 30, rate: 0.5, temp: .absolute, timestamp: currentTime)
|
|
|
+
|
|
|
+ let result = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+
|
|
|
+ #expect(result?.rate == 0.5)
|
|
|
+ #expect(result?.duration == 30)
|
|
|
+ #expect(result?.reason.contains("doing nothing") == true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 5 from JS
|
|
|
+ @Test("should error if target_bg cannot be determined") func errorIfTargetBGMissing() throws {
|
|
|
+ var (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ currentTemp,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ glucoseStatus,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ profile.minBg = nil
|
|
|
+
|
|
|
+ #expect(throws: DeterminationError.missingMinBg) { // Using a placeholder error
|
|
|
+ _ = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 6 from JS
|
|
|
+ @Test("should cancel temp if currenttemp and lastTemp from pumphistory do not match") func cancelTempMismatch() throws {
|
|
|
+ let (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ _,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ glucoseStatus,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
|
|
|
+
|
|
|
+ let lastTempTime = currentTime.addingTimeInterval(-15 * 60)
|
|
|
+ let lastTemp = IobResult.LastTemp(
|
|
|
+ rate: 1.0,
|
|
|
+ timestamp: lastTempTime,
|
|
|
+ started_at: lastTempTime,
|
|
|
+ date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
|
|
|
+ duration: 30
|
|
|
+ )
|
|
|
+
|
|
|
+ var mutableIobData = iobData
|
|
|
+ mutableIobData[0].lastTemp = lastTemp
|
|
|
+
|
|
|
+ let result = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: mutableIobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+
|
|
|
+ #expect(result?.rate == 0)
|
|
|
+ #expect(result?.duration == 0)
|
|
|
+ // Note: In swift we use a different reason then JS
|
|
|
+ #expect(
|
|
|
+ result?
|
|
|
+ .reason ==
|
|
|
+ "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 7 from JS
|
|
|
+ @Test("should cancel temp if lastTemp from pumphistory ended long ago") func cancelTempOldLastTemp() throws {
|
|
|
+ let (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ _,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ glucoseStatus,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
|
|
|
+
|
|
|
+ let lastTempTime = currentTime.addingTimeInterval(-40 * 60)
|
|
|
+ let lastTemp = IobResult.LastTemp(
|
|
|
+ rate: 1.5,
|
|
|
+ timestamp: lastTempTime,
|
|
|
+ started_at: lastTempTime,
|
|
|
+ date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
|
|
|
+ duration: 30
|
|
|
+ )
|
|
|
+
|
|
|
+ var mutableIobData = iobData
|
|
|
+ mutableIobData[0].lastTemp = lastTemp
|
|
|
+
|
|
|
+ let result = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: mutableIobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+
|
|
|
+ #expect(result?.rate == 0)
|
|
|
+ #expect(result?.duration == 0)
|
|
|
+ // Note: In swift we use a different reason then JS
|
|
|
+ #expect(
|
|
|
+ result?
|
|
|
+ .reason ==
|
|
|
+ "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test 8 from JS
|
|
|
+ @Test("should throw error if eventualBG cannot be calculated") func eventualBGNaN() throws {
|
|
|
+ var (
|
|
|
+ profile,
|
|
|
+ preferences,
|
|
|
+ currentTemp,
|
|
|
+ iobData,
|
|
|
+ mealData,
|
|
|
+ autosensData,
|
|
|
+ reservoirData,
|
|
|
+ glucoseStatus,
|
|
|
+ trioCustomOrefVariables,
|
|
|
+ currentTime
|
|
|
+ ) = createDefaultInputs()
|
|
|
+ profile.sens = .nan
|
|
|
+
|
|
|
+ #expect(throws: DeterminationError.eventualGlucoseCalculationError(sensitivity: .nan, deviation: .nan)) {
|
|
|
+ _ = try DeterminationGenerator.determineBasal(
|
|
|
+ profile: profile,
|
|
|
+ preferences: preferences,
|
|
|
+ currentTemp: currentTemp,
|
|
|
+ iobData: iobData,
|
|
|
+ mealData: mealData,
|
|
|
+ autosensData: autosensData,
|
|
|
+ reservoirData: reservoirData,
|
|
|
+ glucoseStatus: glucoseStatus,
|
|
|
+ trioCustomOrefVariables: trioCustomOrefVariables,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|