Просмотр исходного кода

feat(algo): First jab at finalizing last dosing decision(s)

Deniz Cengiz 6 месяцев назад
Родитель
Сommit
a14b9eed39

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -712,6 +712,7 @@
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */; };
 		DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */; };
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
+		DD758EDE2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD758EDD2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
@@ -1650,6 +1651,7 @@
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentMenuView.swift; sourceTree = "<group>"; };
 		DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentMenuView.swift; sourceTree = "<group>"; };
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
+		DD758EDD2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalSmbMicroBolusTests.swift; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
@@ -2973,6 +2975,7 @@
 				3B3B4F572E662B1500B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift */,
 				3B3B4F572E662B1500B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
 				3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */,
 				3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */,
+				DD758EDD2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
 				3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */,
 				3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */,
 				3B2CE68A2E24ADF3005EF782 /* IobGenerateTests.swift */,
 				3B2CE68A2E24ADF3005EF782 /* IobGenerateTests.swift */,
@@ -5291,6 +5294,7 @@
 				3B1C5C432D68E269004E9273 /* Extensions.swift in Sources */,
 				3B1C5C432D68E269004E9273 /* Extensions.swift in Sources */,
 				3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */,
 				3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */,
 				3BF92F382D86E10B006B545A /* OpenAPSFixed.swift in Sources */,
 				3BF92F382D86E10B006B545A /* OpenAPSFixed.swift in Sources */,
+				DD758EDE2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift in Sources */,
 				3B4821822E080CAE00F0DD17 /* HttpFiles.swift in Sources */,
 				3B4821822E080CAE00F0DD17 /* HttpFiles.swift in Sources */,
 				3B1C5C442D68E269004E9273 /* IobJsonTypes.swift in Sources */,
 				3B1C5C442D68E269004E9273 /* IobJsonTypes.swift in Sources */,
 				3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */,
 				3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */,

+ 168 - 3
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -21,6 +21,7 @@ enum DeterminationGenerator {
         autosensData: Autosens,
         autosensData: Autosens,
         reservoirData: Decimal,
         reservoirData: Decimal,
         glucose: [BloodGlucose],
         glucose: [BloodGlucose],
+        microBolusAllowed: Bool,
         trioCustomOrefVariables: TrioCustomOrefVariables,
         trioCustomOrefVariables: TrioCustomOrefVariables,
         currentTime: Date
         currentTime: Date
     ) throws -> Determination? {
     ) throws -> Determination? {
@@ -35,6 +36,7 @@ enum DeterminationGenerator {
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -52,6 +54,7 @@ enum DeterminationGenerator {
         autosensData: Autosens,
         autosensData: Autosens,
         reservoirData _: Decimal,
         reservoirData _: Decimal,
         glucoseStatus: GlucoseStatus,
         glucoseStatus: GlucoseStatus,
+        microBolusAllowed: Bool,
         trioCustomOrefVariables: TrioCustomOrefVariables,
         trioCustomOrefVariables: TrioCustomOrefVariables,
         currentTime: Date
         currentTime: Date
     ) throws -> Determination? {
     ) throws -> Determination? {
@@ -446,10 +449,172 @@ enum DeterminationGenerator {
             return determination
             return determination
         }
         }
 
 
-        // TODO: how to handle output?
-        // TODO: how to handle logging?
+        var insulinRequired = (
+            (
+                min(forecastResult.minForecastedGlucose, forecastResult.eventualGlucose) - adjustedGlucoseTargets
+                    .targetGlucose
+            )
+                / adjustedSensitivity
+        )
+        .jsRounded(scale: 2)
+
+        if insulinRequired > profile.maxIob - currentIob {
+            determination.reason += "max_iob \(profile.maxIob), "
+            insulinRequired = (profile.maxIob - currentIob).jsRounded(scale: 2)
+        }
+
+        var rate = basal + (2 * insulinRequired)
+        rate = TempBasalFunctions.roundBasal(profile: profile, basalRate: rate)
+
+        determination.insulinReq = insulinRequired
+
+        // minutes since last bolus
+        let lastBolusAge: Decimal?
+        if let lastBolusTime = iobData.first?.lastBolusTime {
+            let millisecondsSince1970 = Decimal(currentTime.timeIntervalSince1970 * 1000)
+            lastBolusAge = ((millisecondsSince1970 - Decimal(lastBolusTime)) / 60000).jsRounded(scale: 1)
+        } else {
+            lastBolusAge = nil
+        }
+
+        let minIOBForecastedGlucose = forecastResult.iob.min() ?? 0
+
+        // only allow microboluses with COB or low temp targets, or within DIA hours of a bolus
+        if microBolusAllowed, smbIsEnabled, currentGlucose > threshold {
+            let carbRatio = profile.carbRatio ?? profile.carbRatioFor(time: currentTime)
+            let currentBasal = profile.currentBasal ?? profile.basalFor(time: currentTime)
+            let mealInsulinRequired = (mealData.mealCOB / carbRatio).jsRounded(scale: 3)
+
+            let maxBolusCondition: Bool = currentIob > mealInsulinRequired && currentIob > 0
+            let maxBolus: Decimal =
+                (
+                    maxBolusCondition ? (currentBasal * profile.maxUAMSMBBasalMinutes / 60) :
+                        (currentBasal * profile.maxSMBBasalMinutes / 60)
+                ).jsRounded(scale: 1)
+
+            // FIXME: we technically do no longer need this NaN check for the 2 basal minutes settings -> our swift coding is much safer, value will always be present -- below code ported straight from JS in Swift syntax
+
+//                    if profile.maxSMBBasalMinutes.isNaN {
+//                        maxBolus = (currentBasal * 30 / 60).jsRounded(scale: 1)
+//                    } else if currentIob > mealInsulinRequired, currentIob > 0 {
+//                        if profile.maxUAMSMBBasalMinutes.isNaN {
+//                            maxBolus = (currentBasal * 30 / 60).jsRounded(scale: 1)
+//                        } else {
+//                            maxBolus = (currentBasal * profile.maxUAMSMBBasalMinutes / 60).jsRounded(scale: 1)
+//                        }
+//                    } else {
+//                        maxBolus = (currentBasal * profile.maxSMBBasalMinutes / 60).jsRounded(scale: 1)
+//                    }
+
+            // FIXME: round this to allowed pump increments?
+            let microBolus = min(insulinRequired / 2, maxBolus)
+
+            let smbTarget = adjustedGlucoseTargets.targetGlucose
+            let worstCaseInsulinRequired = (smbTarget - (naiveEventualGlucose + minIOBForecastedGlucose) / 2) /
+                adjustedSensitivity
+            var durationRequired = (60 * worstCaseInsulinRequired / currentBasal).jsRounded()
+
+            // if insulinRequired > 0 but not enough for a microBolus, don't set an SMB zero temp
+            if insulinRequired > 0, microBolus < profile.bolusIncrement {
+                durationRequired = 0
+            }
+
+            var smbLowTempRequired: Decimal = 0
+            if durationRequired <= 0 {
+                durationRequired = 0
+            } else if durationRequired >= 30 {
+                durationRequired = (durationRequired / 30).jsRounded() * 30
+                durationRequired = min(60, max(0, durationRequired))
+            } else {
+                smbLowTempRequired = (basal * durationRequired / 30).jsRounded(scale: 2)
+                durationRequired = 30
+            }
+
+            determination.reason += " insulinReq \(insulinRequired)"
+            if microBolus >= maxBolus {
+                determination.reason += "; maxBolus \(maxBolus)"
+            }
+            if durationRequired > 0 {
+                determination.reason += "; setting \(durationRequired)m low temp of \(smbLowTempRequired)U/h"
+            }
+            determination.reason += ". "
+
+            var smbInterval: Decimal = 3
+            if !profile.smbInterval.isNaN {
+                smbInterval = min(10, max(1, profile.smbInterval))
+            }
+
+            if let lastBolusAge {
+                let nextBolusMinutes = smbInterval - lastBolusAge
+                let nextBolusSeconds = (Int(smbInterval - lastBolusAge) * 60) % 60
+
+                if lastBolusAge > smbInterval {
+                    if microBolus > 0 {
+                        determination.units = microBolus
+                        determination.reason += "Microbolusing \(microBolus)U. "
+                    }
+                } else {
+                    determination.reason += "Waiting \(nextBolusMinutes)m \(nextBolusSeconds)s to microbolus again. "
+                }
+            }
 
 
-        return determination
+            if durationRequired > 0 {
+                determination.rate = smbLowTempRequired
+                determination.duration = durationRequired
+                return determination
+            }
+        }
+
+        let maxSafeBasal = try TempBasalFunctions.getMaxSafeBasalRate(profile: profile)
+
+        if rate > maxSafeBasal {
+            determination.reason += "adj. req. rate: \(rate) to maxSafeBasal: \(maxSafeBasal), "
+            rate = TempBasalFunctions.roundBasal(profile: profile, basalRate: maxSafeBasal)
+        }
+
+        let insulinScheduled = Decimal(currentTemp.duration) * (currentTemp.rate - basal) / 60
+        if insulinScheduled >= insulinRequired * 2 {
+            determination.reason +=
+                "\(currentTemp.duration)m@\(currentTemp.rate.jsRounded(scale: 2)) > 2 * insulinReq. Setting temp basal of \(rate)U/hr. "
+            let finalDetermination = try TempBasalFunctions.setTempBasal(
+                rate: rate,
+                duration: 30,
+                profile: profile,
+                determination: determination,
+                currentTemp: currentTemp
+            )
+            return finalDetermination
+        }
+
+        if currentTemp.duration == 0 {
+            determination.reason += "no temp, setting \(rate)U/hr. "
+            let finalDetermination = try TempBasalFunctions.setTempBasal(
+                rate: rate,
+                duration: 30,
+                profile: profile,
+                determination: determination,
+                currentTemp: currentTemp
+            )
+            return finalDetermination
+        }
+
+        let roundedRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: rate)
+        let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
+
+        if currentTemp.duration > 5, roundedRate <= roundedCurrentRate {
+            determination.reason += "temp \(currentTemp.rate) >~ req \(rate)U/hr. "
+            return determination
+        }
+
+        determination.reason += "temp \(currentTemp.rate)<\(rate)U/hr. "
+        let finalDetermination = try TempBasalFunctions.setTempBasal(
+            rate: rate,
+            duration: 30,
+            profile: profile,
+            determination: determination,
+            currentTemp: currentTemp
+        )
+        return finalDetermination
     }
     }
 
 
     static func checkDeterminationInputs(
     static func checkDeterminationInputs(

+ 1 - 0
Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift

@@ -103,6 +103,7 @@ struct OpenAPSSwift {
                 autosensData: autosensData,
                 autosensData: autosensData,
                 reservoirData: reservoir ?? 100,
                 reservoirData: reservoir ?? 100,
                 glucose: glucose,
                 glucose: glucose,
+                microBolusAllowed: microBolusAllowed,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 currentTime: clock
                 currentTime: clock
             )
             )

+ 2 - 2
Trio/Sources/Models/Determination.swift

@@ -7,8 +7,8 @@ struct DeterminationErrorResponse: JSON, Equatable {
 struct Determination: JSON, Equatable {
 struct Determination: JSON, Equatable {
     let id: UUID?
     let id: UUID?
     var reason: String
     var reason: String
-    let units: Decimal?
-    let insulinReq: Decimal?
+    var units: Decimal?
+    var insulinReq: Decimal?
     var eventualBG: Int?
     var eventualBG: Int?
     let sensitivityRatio: Decimal?
     let sensitivityRatio: Decimal?
     var rate: Decimal?
     var rate: Decimal?

+ 22 - 0
TrioTests/OpenAPSSwiftTests/DetermineBasalEarlyExitTests.swift

@@ -12,6 +12,7 @@ import Testing
         autosensData: Autosens,
         autosensData: Autosens,
         reservoirData: Decimal,
         reservoirData: Decimal,
         glucoseStatus: GlucoseStatus,
         glucoseStatus: GlucoseStatus,
+        microBolusAllowed: Bool,
         trioCustomOrefVariables: TrioCustomOrefVariables,
         trioCustomOrefVariables: TrioCustomOrefVariables,
         currentTime: Date
         currentTime: Date
     ) {
     ) {
@@ -125,6 +126,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: 100,
             reservoirData: 100,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: true,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -141,6 +143,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             glucoseStatus,
             glucoseStatus,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -157,6 +160,7 @@ import Testing
                 autosensData: autosensData,
                 autosensData: autosensData,
                 reservoirData: reservoirData,
                 reservoirData: reservoirData,
                 glucoseStatus: glucoseStatus,
                 glucoseStatus: glucoseStatus,
+                microBolusAllowed: microBolusAllowed,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 currentTime: currentTime
                 currentTime: currentTime
             )
             )
@@ -174,6 +178,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             _,
             _,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -199,6 +204,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -219,6 +225,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             _,
             _,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -245,6 +252,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -265,6 +273,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             _,
             _,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -291,6 +300,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -311,6 +321,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             glucoseStatus,
             glucoseStatus,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -326,6 +337,7 @@ import Testing
                 autosensData: autosensData,
                 autosensData: autosensData,
                 reservoirData: reservoirData,
                 reservoirData: reservoirData,
                 glucoseStatus: glucoseStatus,
                 glucoseStatus: glucoseStatus,
+                microBolusAllowed: microBolusAllowed,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 currentTime: currentTime
                 currentTime: currentTime
             )
             )
@@ -343,6 +355,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             glucoseStatus,
             glucoseStatus,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -369,6 +382,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -394,6 +408,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             glucoseStatus,
             glucoseStatus,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -420,6 +435,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -445,6 +461,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             glucoseStatus,
             glucoseStatus,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -460,6 +477,7 @@ import Testing
                 autosensData: autosensData,
                 autosensData: autosensData,
                 reservoirData: reservoirData,
                 reservoirData: reservoirData,
                 glucoseStatus: glucoseStatus,
                 glucoseStatus: glucoseStatus,
+                microBolusAllowed: microBolusAllowed,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 trioCustomOrefVariables: trioCustomOrefVariables,
                 currentTime: currentTime
                 currentTime: currentTime
             )
             )
@@ -477,6 +495,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             _,
             _,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             currentTime
             currentTime
         ) = createDefaultInputs()
         ) = createDefaultInputs()
@@ -501,6 +520,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )
@@ -526,6 +546,7 @@ import Testing
             autosensData,
             autosensData,
             reservoirData,
             reservoirData,
             glucoseStatus,
             glucoseStatus,
+            microBolusAllowed,
             trioCustomOrefVariables,
             trioCustomOrefVariables,
             _
             _
         ) = createDefaultInputs(currentTime: currentTime)
         ) = createDefaultInputs(currentTime: currentTime)
@@ -541,6 +562,7 @@ import Testing
             autosensData: autosensData,
             autosensData: autosensData,
             reservoirData: reservoirData,
             reservoirData: reservoirData,
             glucoseStatus: glucoseStatus,
             glucoseStatus: glucoseStatus,
+            microBolusAllowed: microBolusAllowed,
             trioCustomOrefVariables: trioCustomOrefVariables,
             trioCustomOrefVariables: trioCustomOrefVariables,
             currentTime: currentTime
             currentTime: currentTime
         )
         )

+ 180 - 0
TrioTests/OpenAPSSwiftTests/DetermineBasalSmbMicroBolusTests.swift

@@ -0,0 +1,180 @@
+import Foundation
+import Testing
+@testable import Trio
+
+@Suite("determineBasal SMB microbolus behavior") struct DetermineBasalSmbMicroBolusTests {
+    private func buildInputs(lastBolusOffsetMinutes: Decimal) -> (
+        profile: Profile,
+        preferences: Preferences,
+        currentTemp: TempBasal,
+        iobData: [IobResult],
+        mealData: ComputedCarbs,
+        autosensData: Autosens,
+        reservoirData: Decimal,
+        glucoseStatus: GlucoseStatus,
+        trioCustomOrefVariables: TrioCustomOrefVariables,
+        microBolusAllowed: Bool,
+        currentTime: Date
+    ) {
+        let now = Date()
+
+        var profile = Profile()
+        profile.currentBasal = 1.0
+        profile.maxDailyBasal = 1.5
+        profile.maxBasal = 3.0
+        profile.minBg = 90
+        profile.maxBg = 160
+        profile.targetBg = 100
+        profile.sens = 40
+        profile.carbRatio = 10
+        profile.thresholdSetting = 70
+        profile.maxIob = 6
+        profile.enableSMBAlways = true
+        profile.bolusIncrement = 0.1
+        profile.enableSMBHighBg = true
+        profile.enableSMBHighBgTarget = 140
+
+        var preferences = Preferences()
+        preferences.curve = .rapidActing
+        preferences.useCustomPeakTime = false
+
+        let currentTemp = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: now)
+
+        let lastBolusTime = UInt64(
+            now
+                .addingTimeInterval(TimeInterval(-60 * NSDecimalNumber(decimal: lastBolusOffsetMinutes).doubleValue))
+                .timeIntervalSince1970 * 1000
+        )
+        let iobData = [IobResult(
+            iob: 0.2,
+            activity: 0,
+            basaliob: 0.2,
+            bolusiob: 0,
+            netbasalinsulin: 0,
+            bolusinsulin: 0,
+            time: now,
+            iobWithZeroTemp: IobResult.IobWithZeroTemp(
+                iob: 0.2,
+                activity: 0,
+                basaliob: 0.2,
+                bolusiob: 0,
+                netbasalinsulin: 0,
+                bolusinsulin: 0,
+                time: now
+            ),
+            lastBolusTime: lastBolusTime,
+            lastTemp: IobResult.LastTemp(
+                rate: 0,
+                timestamp: now,
+                started_at: now,
+                date: UInt64(now.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: 5,
+            glucose: 190,
+            noise: 1,
+            shortAvgDelta: 5,
+            longAvgDelta: 5,
+            date: now,
+            lastCalIndex: nil,
+            device: "test"
+        )
+
+        let trioCustomOrefVariables = TrioCustomOrefVariables(
+            average_total_data: 0,
+            weightedAverage: 0,
+            currentTDD: 0,
+            past2hoursAverage: 0,
+            date: now,
+            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: 0,
+            glucoseStatus: glucoseStatus,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            microBolusAllowed: true,
+            currentTime: now
+        )
+    }
+
+    @Test("Applies SMB microbolus when interval has elapsed") func testMicrobolusWhenIntervalElapsed() throws {
+        let inputs = buildInputs(lastBolusOffsetMinutes: 10)
+
+        let determination = try DeterminationGenerator.determineBasal(
+            profile: inputs.profile,
+            preferences: inputs.preferences,
+            currentTemp: inputs.currentTemp,
+            iobData: inputs.iobData,
+            mealData: inputs.mealData,
+            autosensData: inputs.autosensData,
+            reservoirData: inputs.reservoirData,
+            glucoseStatus: inputs.glucoseStatus,
+            microBolusAllowed: inputs.microBolusAllowed,
+            trioCustomOrefVariables: inputs.trioCustomOrefVariables,
+            currentTime: inputs.currentTime
+        )
+
+        #expect(determination?.units ?? 0 > 0)
+        #expect(determination?.reason.contains("Microbolusing") ?? false)
+    }
+
+    @Test("Waits for SMB interval before another microbolus") func testWaitsForInterval() throws {
+        let inputs = buildInputs(lastBolusOffsetMinutes: 1)
+
+        let determination = try DeterminationGenerator.determineBasal(
+            profile: inputs.profile,
+            preferences: inputs.preferences,
+            currentTemp: inputs.currentTemp,
+            iobData: inputs.iobData,
+            mealData: inputs.mealData,
+            autosensData: inputs.autosensData,
+            reservoirData: inputs.reservoirData,
+            glucoseStatus: inputs.glucoseStatus,
+            microBolusAllowed: inputs.microBolusAllowed,
+            trioCustomOrefVariables: inputs.trioCustomOrefVariables,
+            currentTime: inputs.currentTime
+        )
+
+        #expect(determination?.units == nil || determination?.units == 0)
+        #expect(determination?.reason.contains("Waiting") ?? false)
+    }
+}