Kaynağa Gözat

Port glucose falling faster than expected logic

Sam King 8 ay önce
ebeveyn
işleme
6c6305aee5

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -225,6 +225,7 @@
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
 		3B31D5742E0E26C00047D32D /* ReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B31D5732E0E26BB0047D32D /* ReplayTests.swift */; };
 		3B31D5742E0E26C00047D32D /* ReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B31D5732E0E26BB0047D32D /* ReplayTests.swift */; };
+		3B3B4F542E661A1200B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3B4F532E6619FF00B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift */; };
 		3B3B57C92DA07B3400849D16 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */; };
 		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 */; };
 		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 */; };
 		3B4550532D862C0000551B0D /* PumpHistoryEvent+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */; };
@@ -1175,6 +1176,7 @@
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
 		3B31D5732E0E26BB0047D32D /* ReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayTests.swift; sourceTree = "<group>"; };
 		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>"; };
 		3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; 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>"; };
 		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>"; };
 		3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpHistoryEvent+Duplicates.swift"; sourceTree = "<group>"; };
@@ -2951,6 +2953,7 @@
 				DD30B9FF2E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift */,
 				DD30B9FF2E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift */,
 				3B8221B12E5882D900585156 /* DetermineBasalEarlyExitTests.swift */,
 				3B8221B12E5882D900585156 /* DetermineBasalEarlyExitTests.swift */,
 				3BAC929A2E55FF4F00B853DA /* DetermineBasalEnableSmbTests.swift */,
 				3BAC929A2E55FF4F00B853DA /* DetermineBasalEnableSmbTests.swift */,
+				3B3B4F532E6619FF00B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
 				3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */,
 				3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
@@ -5222,6 +5225,7 @@
 				3B8B5D3E2DF5240C00365ED3 /* TimeZoneForTests.swift in Sources */,
 				3B8B5D3E2DF5240C00365ED3 /* TimeZoneForTests.swift in Sources */,
 				3B1DB90F2E63C14C00BD814B /* DetermineBasalLowEventualGlucoseTests.swift in Sources */,
 				3B1DB90F2E63C14C00BD814B /* DetermineBasalLowEventualGlucoseTests.swift in Sources */,
 				3B5CD2CA2D4AECD500CE213C /* ProfileJavascriptTests.swift in Sources */,
 				3B5CD2CA2D4AECD500CE213C /* ProfileJavascriptTests.swift in Sources */,
+				3B3B4F542E661A1200B668E3 /* DetermineBasalGlucoseFallingFasterThanExpectedTests.swift in Sources */,
 				3B5CD2CB2D4AECD500CE213C /* ProfileTargetsTests.swift in Sources */,
 				3B5CD2CB2D4AECD500CE213C /* ProfileTargetsTests.swift in Sources */,
 				3BAC92CD2E57859D00B853DA /* SetTempBasalTests.swift in Sources */,
 				3BAC92CD2E57859D00B853DA /* SetTempBasalTests.swift in Sources */,
 				3B5CD2CD2D4AECD500CE213C /* ProfileIsfTests.swift in Sources */,
 				3B5CD2CD2D4AECD500CE213C /* ProfileIsfTests.swift in Sources */,

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

@@ -389,6 +389,26 @@ enum DeterminationGenerator {
             return determination
             return determination
         }
         }
 
 
+        let (
+            shouldSetTempBasalForGlucoseFallingFasterThanExpected,
+            glucoseFallingFasterThanExpectedDetermination
+        ) = try DosingEngine.glucoseFallingFasterThanExpected(
+            eventualGlucose: forecastResult.eventualGlucose,
+            minGlucose: adjustedGlucoseTargets.minGlucose,
+            minDelta: minDelta,
+            expectedDelta: expectedDelta,
+            glucoseStatus: glucoseStatus,
+            currentTemp: currentTemp,
+            basal: basal,
+            smbIsEnabled: smbIsEnabled,
+            profile: profile,
+            determination: determination
+        )
+        determination = glucoseFallingFasterThanExpectedDetermination
+        if shouldSetTempBasalForGlucoseFallingFasterThanExpected {
+            return determination
+        }
+
         // TODO: how to handle output?
         // TODO: how to handle output?
         // TODO: how to handle logging?
         // TODO: how to handle logging?
 
 

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

@@ -523,4 +523,60 @@ enum DosingEngine {
             return (shouldSetTempBasal: true, determination: finalDetermination)
             return (shouldSetTempBasal: true, determination: finalDetermination)
         }
         }
     }
     }
+
+    /// Handles the case where glucose is falling faster than expected.
+    ///
+    /// - 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 glucoseFallingFasterThanExpected(
+        eventualGlucose: Decimal,
+        minGlucose: Decimal,
+        minDelta: Decimal,
+        expectedDelta: Decimal,
+        glucoseStatus: GlucoseStatus,
+        currentTemp: TempBasal,
+        basal: Decimal,
+        smbIsEnabled: Bool,
+        profile: Profile,
+        determination: Determination
+    ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
+        guard minDelta < expectedDelta else {
+            return (shouldSetTempBasal: false, determination: determination)
+        }
+
+        var newDetermination = determination
+
+        if !smbIsEnabled {
+            if glucoseStatus.delta < minDelta {
+                newDetermination
+                    .reason +=
+                    "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+            } else {
+                newDetermination
+                    .reason +=
+                    "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Min. Delta \(minDelta.jsRounded(scale: 2)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+            }
+
+            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)
+            }
+        }
+
+        return (shouldSetTempBasal: false, determination: determination)
+    }
 }
 }

+ 130 - 0
TrioTests/OpenAPSSwiftTests/DetermineBasalGlucoseFallingFasterThanExpectedTests.swift

@@ -0,0 +1,130 @@
+
+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-glucose-falling-faster-than-expected.test.js
+@Suite("DosingEngine.glucoseFallingFasterThanExpected") struct DetermineBasalGlucoseFallingFasterThanExpectedTests {
+    private func defaultProfile() -> Profile {
+        var profile = Profile()
+        profile.minBg = 90
+        profile.targetBg = 100
+        profile.currentBasal = 1.0
+        profile.maxDailyBasal = 1.3
+        profile.maxBasal = 3.5
+        profile.sens = 50
+        profile.outUnits = .mgdL
+        return profile
+    }
+
+    private func defaultGlucoseStatus() -> GlucoseStatus {
+        GlucoseStatus(
+            delta: 5,
+            glucose: 100,
+            noise: 1,
+            shortAvgDelta: 0,
+            longAvgDelta: 0,
+            date: Date(),
+            lastCalIndex: nil,
+            device: "test"
+        )
+    }
+
+    private func callGlucoseFallingFasterThanExpected(
+        eventualGlucose: Decimal = 100,
+        minGlucose: Decimal? = nil,
+        minDelta: Decimal = 4,
+        expectedDelta: Decimal = 5,
+        glucoseStatus: GlucoseStatus? = nil,
+        currentTemp: TempBasal = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date()),
+        basal: Decimal? = nil,
+        smbIsEnabled: Bool = false,
+        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.glucoseFallingFasterThanExpected(
+            eventualGlucose: eventualGlucose,
+            minGlucose: minGlucose ?? testProfile.minBg!,
+            minDelta: minDelta,
+            expectedDelta: expectedDelta,
+            glucoseStatus: glucoseStatus ?? defaultGlucoseStatus(),
+            currentTemp: currentTemp,
+            basal: basal ?? testProfile.currentBasal!,
+            smbIsEnabled: smbIsEnabled,
+            profile: testProfile,
+            determination: testDetermination
+        )
+    }
+
+    @Test("Guard: minDelta not less than expectedDelta") func testMinDeltaNotLessThanExpected() throws {
+        let (shouldSet, determination) = try callGlucoseFallingFasterThanExpected(minDelta: 5, expectedDelta: 5)
+        #expect(shouldSet == false)
+        #expect(determination.reason == "")
+    }
+
+    @Test("Guard: SMB is enabled") func testSmbIsEnabled() throws {
+        let (shouldSet, determination) = try callGlucoseFallingFasterThanExpected(smbIsEnabled: true)
+        #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 callGlucoseFallingFasterThanExpected(
+            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") func testSetNewTemp() 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 callGlucoseFallingFasterThanExpected(
+            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."))
+    }
+}