| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- import Foundation
- import Testing
- @testable import Trio
- @Suite("DetermineBasal early exits before core dosing logic") struct DetermineBasalEarlyExitTests {
- private func createDefaultInputs(currentTime: Date = Date()) -> (
- profile: Profile,
- preferences: Preferences,
- currentTemp: TempBasal,
- iobData: [IobResult],
- mealData: ComputedCarbs,
- autosensData: Autosens,
- reservoirData: Decimal,
- glucoseStatus: GlucoseStatus,
- microBolusAllowed: Bool,
- trioCustomOrefVariables: TrioCustomOrefVariables,
- 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
- )
- return (
- profile: profile,
- preferences: preferences,
- currentTemp: currentTemp,
- iobData: iobData,
- mealData: mealData,
- autosensData: autosensData,
- reservoirData: 100,
- glucoseStatus: glucoseStatus,
- microBolusAllowed: true,
- 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,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- 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,
- _,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- trioCustomOrefVariables: trioCustomOrefVariables,
- currentTime: currentTime
- )
- #expect(result?.rate == profile.currentBasal)
- #expect(result?.duration == 30)
- #expect(result?.reason.contains("Replacing 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,
- _,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- 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,
- _,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- trioCustomOrefVariables: trioCustomOrefVariables,
- currentTime: currentTime
- )
- #expect(result?.rate == nil)
- #expect(result?.duration == nil)
- #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,
- microBolusAllowed,
- trioCustomOrefVariables,
- currentTime
- ) = createDefaultInputs()
- profile.minBg = nil
- #expect(throws: DeterminationError.invalidProfileTarget) {
- _ = try DeterminationGenerator.determineBasal(
- profile: profile,
- preferences: preferences,
- currentTemp: currentTemp,
- iobData: iobData,
- mealData: mealData,
- autosensData: autosensData,
- reservoirData: reservoirData,
- glucoseStatus: glucoseStatus,
- microBolusAllowed: microBolusAllowed,
- 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,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- 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 ==
- "Warning: currenttemp rate 1.5 != lastTemp rate 1 from pumphistory; canceling temp"
- )
- }
- // 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,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- 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 == "Warning: currenttemp running but lastTemp from pumphistory ended 10m ago; canceling temp"
- )
- }
- // 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,
- microBolusAllowed,
- 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,
- microBolusAllowed: microBolusAllowed,
- trioCustomOrefVariables: trioCustomOrefVariables,
- currentTime: currentTime
- )
- }
- }
- // Test 9 from JS
- @Test("should low-temp if BG is below threshold") func lowGlucoseSuspend() throws {
- let (
- profile,
- preferences,
- currentTemp,
- iobData,
- mealData,
- autosensData,
- reservoirData,
- _,
- microBolusAllowed,
- trioCustomOrefVariables,
- currentTime
- ) = createDefaultInputs()
- let glucoseStatus = GlucoseStatus(
- delta: 0,
- glucose: 70,
- noise: 1,
- shortAvgDelta: 0,
- longAvgDelta: 0.1,
- date: currentTime,
- lastCalIndex: nil,
- device: "test"
- )
- let result = try DeterminationGenerator.determineBasal(
- profile: profile,
- preferences: preferences,
- currentTemp: currentTemp,
- iobData: iobData,
- mealData: mealData,
- autosensData: autosensData,
- reservoirData: reservoirData,
- glucoseStatus: glucoseStatus,
- microBolusAllowed: microBolusAllowed,
- trioCustomOrefVariables: trioCustomOrefVariables,
- currentTime: currentTime
- )
- #expect(result?.rate == 0)
- #expect((result?.duration ?? 0) >= 30)
- #expect(result?.reason.contains("minGuardBG") == true)
- }
- // Test 10 from JS
- @Test("should cancel temp before the hour if not doing SMB") func skipNeutralTemp() throws {
- // Create a date that is 56 minutes past the hour
- var components = Calendar.current.dateComponents(in: .current, from: Date())
- components.minute = 56
- let currentTime = Calendar.current.date(from: components)!
- var (
- profile,
- preferences,
- currentTemp,
- iobData,
- mealData,
- autosensData,
- reservoirData,
- glucoseStatus,
- microBolusAllowed,
- trioCustomOrefVariables,
- _
- ) = createDefaultInputs(currentTime: currentTime)
- profile.skipNeutralTemps = true
- let result = try DeterminationGenerator.determineBasal(
- profile: profile,
- preferences: preferences,
- currentTemp: currentTemp,
- iobData: iobData,
- mealData: mealData,
- autosensData: autosensData,
- reservoirData: reservoirData,
- glucoseStatus: glucoseStatus,
- microBolusAllowed: microBolusAllowed,
- trioCustomOrefVariables: trioCustomOrefVariables,
- currentTime: currentTime
- )
- #expect(result?.rate == 0)
- #expect(result?.duration == 0)
- #expect(result?.reason.contains("Canceling temp") == true)
- }
- }
|