Kaynağa Gözat

Port iob greater than max logic

Sam King 8 ay önce
ebeveyn
işleme
57cd775c37

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -227,6 +227,7 @@
 		3B31D5742E0E26C00047D32D /* ReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B31D5732E0E26BB0047D32D /* ReplayTests.swift */; };
 		3B3B4F542E661A1200B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3B4F532E6619FF00B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift */; };
 		3B3B4F562E661FD600B668E3 /* DetermineBasalEventualOrForecastGlucoseLessThanMaxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3B4F552E661FC700B668E3 /* DetermineBasalEventualOrForecastGlucoseLessThanMaxTests.swift */; };
+		3B3B4F582E662B1E00B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3B4F572E662B1500B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift */; };
 		3B3B57C92DA07B3400849D16 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */; };
 		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
 		3B4550532D862C0000551B0D /* PumpHistoryEvent+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */; };
@@ -1179,6 +1180,7 @@
 		3B31D5732E0E26BB0047D32D /* ReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayTests.swift; sourceTree = "<group>"; };
 		3B3B4F532E6619FF00B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalGlucoseFallingFasterThanExpectedTests.swift; sourceTree = "<group>"; };
 		3B3B4F552E661FC700B668E3 /* DetermineBasalEventualOrForecastGlucoseLessThanMaxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalEventualOrForecastGlucoseLessThanMaxTests.swift; sourceTree = "<group>"; };
+		3B3B4F572E662B1500B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalIobGreaterThanMaxTests.swift; sourceTree = "<group>"; };
 		3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
 		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
 		3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpHistoryEvent+Duplicates.swift"; sourceTree = "<group>"; };
@@ -2957,6 +2959,7 @@
 				3BAC929A2E55FF4F00B853DA /* DetermineBasalEnableSmbTests.swift */,
 				3B3B4F552E661FC700B668E3 /* DetermineBasalEventualOrForecastGlucoseLessThanMaxTests.swift */,
 				3B3B4F532E6619FF00B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift */,
+				3B3B4F572E662B1500B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
 				3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
@@ -5233,6 +5236,7 @@
 				3BAC92CD2E57859D00B853DA /* SetTempBasalTests.swift in Sources */,
 				3B5CD2CD2D4AECD500CE213C /* ProfileIsfTests.swift in Sources */,
 				3B5CD2CE2D4AECD500CE213C /* ProfileBasalTests.swift in Sources */,
+				3B3B4F582E662B1E00B668E3 /* DetermineBasalIobGreaterThanMaxTests.swift in Sources */,
 				3BFA5BF92D989F510072B082 /* MockTDDStorage.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				3BF8D14B2D530397001B3F84 /* JSONCompareTests.swift in Sources */,

+ 13 - 0
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -433,6 +433,19 @@ enum DeterminationGenerator {
                 "Eventual BG \(DosingEngine.convertGlucose(profile: profile, glucose: forecastResult.eventualGlucose)) >= \(DosingEngine.convertGlucose(profile: profile, glucose: adjustedGlucoseTargets.maxGlucose)), "
         }
 
+        let (shouldSetTempBasalForIobGreaterThanMax, iobGreaterThanMaxDetermination) = try DosingEngine.iobGreaterThanMax(
+            iob: currentIob,
+            maxIob: profile.maxIob,
+            currentTemp: currentTemp,
+            basal: basal,
+            profile: profile,
+            determination: determination
+        )
+        determination = iobGreaterThanMaxDetermination
+        if shouldSetTempBasalForIobGreaterThanMax {
+            return determination
+        }
+
         // TODO: how to handle output?
         // TODO: how to handle logging?
 

+ 39 - 0
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DosingEngine.swift

@@ -628,4 +628,43 @@ enum DosingEngine {
 
         return (shouldSetTempBasal: false, determination: determination)
     }
+
+    /// Handles the case where IOB is greater than the max IOB.
+    ///
+    /// - Returns: A tuple containing:
+    ///   - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
+    ///   - `determination`: The (potentially modified) determination object.
+    static func iobGreaterThanMax(
+        iob: Decimal,
+        maxIob: Decimal,
+        currentTemp: TempBasal,
+        basal: Decimal,
+        profile: Profile,
+        determination: Determination
+    ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
+        guard iob > maxIob else {
+            return (shouldSetTempBasal: false, determination: determination)
+        }
+
+        var newDetermination = determination
+        newDetermination.reason += "IOB \(iob.jsRounded(scale: 2)) > max_iob \(maxIob)"
+
+        let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
+        let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
+
+        if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
+            newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
+            return (shouldSetTempBasal: true, determination: newDetermination)
+        } else {
+            newDetermination.reason += "; setting current basal of \(basal) as temp. "
+            let finalDetermination = try TempBasalFunctions.setTempBasal(
+                rate: basal,
+                duration: 30,
+                profile: profile,
+                determination: newDetermination,
+                currentTemp: currentTemp
+            )
+            return (shouldSetTempBasal: true, determination: finalDetermination)
+        }
+    }
 }

+ 119 - 0
TrioTests/OpenAPSSwiftTests/DetermineBasalIobGreaterThanMaxTests.swift

@@ -0,0 +1,119 @@
+
+import Foundation
+import Testing
+@testable import Trio
+
+/// These tests should be an exact copy of the JS tests here:
+/// - https://github.com/kingst/trio-oref/blob/dev-fixes-for-swift-comparison/tests/determine-basal-iob-greater-than-max.test.js
+@Suite("DosingEngine.iobGreaterThanMax") struct DetermineBasalIobGreaterThanMaxTests {
+    private func defaultProfile() -> Profile {
+        var profile = Profile()
+        profile.maxIob = 1.5
+        profile.currentBasal = 1.0
+        profile.maxBasal = 1.5
+        profile.maxDailyBasal = 3.5
+        profile.outUnits = .mgdL
+        return profile
+    }
+
+    private func callIobGreaterThanMax(
+        iob: Decimal = 1.5,
+        maxIob: Decimal? = nil,
+        currentTemp: TempBasal = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date()),
+        basal: Decimal? = nil,
+        profile: Profile? = nil,
+        determination: Determination? = nil
+    ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
+        let testProfile = profile ?? defaultProfile()
+        let testDetermination = determination ?? Determination(
+            id: nil,
+            reason: "",
+            units: nil,
+            insulinReq: nil,
+            eventualBG: nil,
+            sensitivityRatio: nil,
+            rate: nil,
+            duration: nil,
+            iob: nil,
+            cob: nil,
+            predictions: nil,
+            deliverAt: nil,
+            carbsReq: nil,
+            temp: nil,
+            bg: nil,
+            reservoir: nil,
+            isf: nil,
+            timestamp: nil,
+            tdd: nil,
+            current_target: nil,
+            minDelta: nil,
+            expectedDelta: nil,
+            minGuardBG: nil,
+            minPredBG: nil,
+            threshold: nil,
+            carbRatio: nil,
+            received: nil
+        )
+
+        return try DosingEngine.iobGreaterThanMax(
+            iob: iob,
+            maxIob: maxIob ?? testProfile.maxIob,
+            currentTemp: currentTemp,
+            basal: basal ?? testProfile.currentBasal!,
+            profile: testProfile,
+            determination: testDetermination
+        )
+    }
+
+    @Test("Guard: iob not greater than max") func testIobNotGreaterThanMax() throws {
+        let (shouldSet, determination) = try callIobGreaterThanMax(iob: 1.5, maxIob: 1.5)
+        #expect(shouldSet == false)
+        #expect(determination.reason == "")
+    }
+
+    @Test("Continue current temp") func testContinueCurrentTemp() throws {
+        let profile = defaultProfile()
+        let currentTemp = TempBasal(duration: 20, rate: profile.currentBasal!, temp: .absolute, timestamp: Date())
+        let (shouldSet, determination) = try callIobGreaterThanMax(
+            iob: 1.6,
+            currentTemp: currentTemp,
+            basal: profile.currentBasal!,
+            profile: profile
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == nil) // No change
+        #expect(determination.reason.contains("temp \(currentTemp.rate) ~ req \(profile.currentBasal!)U/hr."))
+    }
+
+    @Test("Set new temp when duration is short") func testSetNewTempDurationShort() throws {
+        let profile = defaultProfile()
+        let currentTemp = TempBasal(duration: 10, rate: 1.0, temp: .absolute, timestamp: Date())
+        let basal: Decimal = 1.2
+        let (shouldSet, determination) = try callIobGreaterThanMax(
+            iob: 1.6,
+            currentTemp: currentTemp,
+            basal: basal,
+            profile: profile
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == basal)
+        #expect(determination.duration == 30)
+        #expect(determination.reason.contains("setting current basal of \(basal) as temp."))
+    }
+
+    @Test("Set new temp when rates differ") func testSetNewTempRatesDiffer() throws {
+        let profile = defaultProfile()
+        let currentTemp = TempBasal(duration: 20, rate: 1.0, temp: .absolute, timestamp: Date())
+        let basal: Decimal = 1.2
+        let (shouldSet, determination) = try callIobGreaterThanMax(
+            iob: 1.6,
+            currentTemp: currentTemp,
+            basal: basal,
+            profile: profile
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == basal)
+        #expect(determination.duration == 30)
+        #expect(determination.reason.contains("setting current basal of \(basal) as temp."))
+    }
+}