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

Merge pull request #616 from nightscout/oref-swift-autosens-bug-fixes

Bug fixes for autosens
Deniz Cengiz 4 месяцев назад
Родитель
Сommit
17f94dbb4e

+ 8 - 0
Trio.xcodeproj/project.pbxproj

@@ -328,6 +328,8 @@
 		3BCA5F7C2DC7B16400A7EAC7 /* pumphistory-with-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */; };
 		3BCE75B32D4B38AE009E9453 /* InsulinSensitivities+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */; };
 		3BCE75B52D4B391F009E9453 /* Decimal+rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */; };
+		3BD433AE2F01CDE600897F7D /* autosens-prepare-24.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BD433AD2F01CDD900897F7D /* autosens-prepare-24.js */; };
+		3BD433B02F01CDF500897F7D /* autosens-prepare-8.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BD433AF2F01CDEC00897F7D /* autosens-prepare-8.js */; };
 		3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */; };
 		3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; };
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
@@ -1268,6 +1270,8 @@
 		3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-with-external.json"; sourceTree = "<group>"; };
 		3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InsulinSensitivities+Convert.swift"; sourceTree = "<group>"; };
 		3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+rounding.swift"; sourceTree = "<group>"; };
+		3BD433AD2F01CDD900897F7D /* autosens-prepare-24.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "autosens-prepare-24.js"; sourceTree = "<group>"; };
+		3BD433AF2F01CDEC00897F7D /* autosens-prepare-8.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "autosens-prepare-8.js"; sourceTree = "<group>"; };
 		3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-24h-zoned.json"; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BE2F1E72E030E2F009E2900 /* MealCobTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealCobTests.swift; sourceTree = "<group>"; };
@@ -3051,6 +3055,8 @@
 			children = (
 				3B214EE92E29631F00046304 /* determine-basal-prepare.js */,
 				3B16C39B2DF75BCB00C5C801 /* autosens-prepare.js */,
+				3BD433AF2F01CDEC00897F7D /* autosens-prepare-8.js */,
+				3BD433AD2F01CDD900897F7D /* autosens-prepare-24.js */,
 				3BC0AA402DA8B8F7000DF7B7 /* iob-history-prepare.js */,
 				3BF92F212D86DEE9006B545A /* autosens.js */,
 				3BF92F222D86DEE9006B545A /* autotune-core.js */,
@@ -4491,6 +4497,7 @@
 				3BF92F2E2D86DEE9006B545A /* autotune-prep.js in Resources */,
 				3BF92F2F2D86DEE9006B545A /* profile.js in Resources */,
 				3BF92F302D86DEE9006B545A /* determine-basal.js in Resources */,
+				3BD433AE2F01CDE600897F7D /* autosens-prepare-24.js in Resources */,
 				3BF92F312D86DEE9006B545A /* meal.js in Resources */,
 				3BF92F322D86DEE9006B545A /* glucose-get-last.js in Resources */,
 				3BF92F332D86DEE9006B545A /* iob.js in Resources */,
@@ -4505,6 +4512,7 @@
 				3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */,
 				DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */,
 				3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */,
+				3BD433B02F01CDF500897F7D /* autosens-prepare-8.js in Resources */,
 				DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */,
 				3B8B5D402DF52D0E00365ED3 /* deviationsUnsorted.json in Resources */,
 				DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */,

+ 6 - 1
Trio/Sources/APS/OpenAPSSwift/Autosens/AutosensGenerator.swift

@@ -110,11 +110,16 @@ struct AutosensGenerator {
             )
 
             debugInfoList.append(Autosens.DebugInfo(
+                iobClock: currGlucose.date,
                 bgi: bgi,
                 iobActivity: iob.activity,
                 deltaGlucose: deltaGlucose,
                 deviation: deviation,
-                stateType: state.type.rawValue
+                stateType: state.type.rawValue,
+                mealCOB: state.mealCOB,
+                absorbing: state.absorbing,
+                mealCarbs: state.mealCarbs,
+                mealStartCounter: state.mealStartCounter
             ))
 
             if state.type == .nonMeal {

+ 1 - 1
Trio/Sources/APS/OpenAPSSwift/Iob/IobHistory.swift

@@ -423,7 +423,7 @@ struct IobHistory {
         let netBasalRate = tempBasalRate - currentRate
         let tempBolusSize: Decimal = netBasalRate < 0 ? -0.05 : 0.05
 
-        let netBasalAmountTmp = (netBasalRate * duration * 10 / 6).rounded()
+        let netBasalAmountTmp = (netBasalRate * duration * 10 / 6).jsRounded()
         let netBasalAmount = netBasalAmountTmp / Decimal(100)
         // FIXME: I think the count should be floor not rounded due to pump implementation artifacts
         let tempBolusCount = Int((netBasalAmount / tempBolusSize).rounded())

+ 2 - 2
Trio/Sources/APS/OpenAPSSwift/Logging/OrefFunction.swift

@@ -105,8 +105,8 @@ enum OrefFunction: String, Codable {
             ]
         case .autosens:
             return [
-                "ratio": 0.011,
-                "newisf": 1.5
+                "ratio": 0.021,
+                "newisf": 3.1
             ]
         case .determineBasal:
             return [

+ 6 - 0
Trio/Sources/Models/Autosens.swift

@@ -2,11 +2,17 @@ import Foundation
 
 struct Autosens: JSON {
     struct DebugInfo: Codable {
+        let iobClock: Date
         let bgi: Decimal
         let iobActivity: Decimal
         let deltaGlucose: Decimal
         let deviation: Decimal
         let stateType: String
+        // COB state for debugging state transitions
+        var mealCOB: Decimal?
+        var absorbing: Bool?
+        var mealCarbs: Decimal?
+        var mealStartCounter: Int?
     }
 
     let ratio: Decimal

+ 162 - 8
TrioTests/OpenAPSSwiftTests/AutosensJsonTests.swift

@@ -25,7 +25,8 @@ import Testing
             profile: try JSONBridge.to(autosensInputs.profile),
             carbs: autosensInputs.carbs,
             temptargets: autosensInputs.tempTargets,
-            clock: autosensInputs.clock
+            clock: autosensInputs.clock,
+            prepareFile: OpenAPSFixed.prepare
         )
 
         let comparison = JSONCompare.createComparison(
@@ -62,16 +63,23 @@ import Testing
         // Extract debug info
         let swiftDebugInfo = swiftDict["debugInfo"] as! [Any]
         let jsDebugInfo = jsDict["debugInfo"] as! [Any]
-        for (s, js) in zip(swiftDebugInfo, jsDebugInfo) {
-            print("Debug Info")
-            print("  - Swift: \(s)")
-            print("  - JS: \(js)")
-        }
 
         // Extract deviationsUnsorted arrays
         let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
         let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
 
+        let combined: [String: Any] = [
+            "swiftDebugInfo": swiftDebugInfo,
+            "jsDebugInfo": jsDebugInfo,
+            "swiftDeviations": swiftDeviations,
+            "jsDeviations": jsDeviations
+        ]
+        let sharedDir = FileManager.default.temporaryDirectory
+        let outputURL = sharedDir.appendingPathComponent("autosens_debug.json")
+        let jsonData = try JSONSerialization.data(withJSONObject: combined, options: .prettyPrinted)
+        try jsonData.write(to: outputURL)
+        print("Writing debug info to: \(outputURL.path)")
+
         // Convert both to Double arrays
         let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
             if let number = value as? NSNumber {
@@ -147,6 +155,11 @@ import Testing
                 continue
             }
 
+            if IobJsonTests.pumpIsSuspended(history: autosensInputs.history) {
+                print("Skipping, known issue with JS and currently suspended pumps")
+                continue
+            }
+
             timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
 
             try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
@@ -156,11 +169,81 @@ import Testing
         }
     }
 
+    @Test("Compare IoB calculation at specific time", .enabled(if: false)) func compareIobAtTime() async throws {
+        // Hard-code the file and time to investigate
+        let filePath = "/files/9e146319-5160-482e-9135-f461b97f1a9f.0.json"
+        let targetClock = Date("2025-09-08T10:42:44.333Z")!
+
+        let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
+        guard let autosensInputs = algorithmComparison.autosensInput else {
+            print("No autosensInputs found")
+            return
+        }
+
+        timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+        let profile = autosensInputs.profile
+
+        // Prepare treatments the same way AutosensGenerator does
+        let swiftTreatments = try IobHistory.calcTempTreatments(
+            history: autosensInputs.history.map { $0.computedEvent() },
+            profile: profile,
+            clock: autosensInputs.clock,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        let encoder = JSONCoding.encoder
+        var output = try encoder.encode(swiftTreatments)
+
+        let sharedDir = FileManager.default.temporaryDirectory
+        var outputURL = sharedDir.appendingPathComponent("swift_treatments.json")
+        try output.write(to: outputURL)
+
+        print("Writing \(outputURL.path)")
+
+        // Set up profile with currentBasal for this time (both Swift and JS autosens do this)
+        var simulationProfile = profile
+        simulationProfile.currentBasal = try Basal.basalLookup(autosensInputs.basalProfile, now: targetClock)
+        simulationProfile.temptargetSet = false
+
+        // Calculate Swift IoB at this time
+        let swiftIob = try IobCalculation.iobTotal(
+            treatments: swiftTreatments,
+            profile: simulationProfile,
+            time: targetClock
+        )
+
+        let openAps = OpenAPSFixed()
+        let jsTreatmentsRaw = try await openAps.iobHistory(
+            pumphistory: autosensInputs.history,
+            profile: try JSONBridge.to(autosensInputs.profile),
+            clock: autosensInputs.clock,
+            autosens: RawJSON.null,
+            zeroTempDuration: RawJSON.null
+        )
+
+        let jsTreatments = try JSONDecoder()
+            .decode([IobJsonTests.IobHistoryResult].self, from: jsTreatmentsRaw.rawJSON.data(using: .utf8)!)
+
+        output = try encoder.encode(jsTreatments)
+        outputURL = sharedDir.appendingPathComponent("js_treatments.json")
+        try output.write(to: outputURL)
+
+        print("Writing \(outputURL.path)")
+
+        print("Swift IoB at \(targetClock):")
+        print("  iob: \(swiftIob.iob)")
+        print("  activity: \(swiftIob.activity)")
+
+        timeZoneForTests.resetTimezone()
+    }
+
     @Test("Format autosens inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
         // this test is meant for one-off analysis so it's ok to hard code
         // a file, just make sure to _not_ check in updates to this to
         // avoid polluting our change logs
-        let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/432be489-adfd-4799-b469-8d3794d5188e.0.json")
+        let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/2084152d-a95e-4d0e-9254-e0951f7aa519.0.json")
         let autosensInputs = algorithmComparison.autosensInput!
 
         let encoder = JSONCoding.encoder
@@ -194,7 +277,8 @@ import Testing
             profile: try JSONBridge.to(autosensInputs.profile),
             carbs: autosensInputs.carbs,
             temptargets: autosensInputs.tempTargets,
-            clock: autosensInputs.clock
+            clock: autosensInputs.clock,
+            prepareFile: OpenAPSFixed.prepare
         )
 
         if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
@@ -205,4 +289,74 @@ import Testing
 
         timeZoneForTests.resetTimezone()
     }
+
+    @Test(
+        "Format autosens inputs for running in JS, 24 hours only",
+        .enabled(if: false)
+    ) func formatInputsFixedTime() async throws {
+        // this test is meant for one-off analysis so it's ok to hard code
+        // a file, just make sure to _not_ check in updates to this to
+        // avoid polluting our change logs
+        let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/2084152d-a95e-4d0e-9254-e0951f7aa519.0.json")
+        let autosensInputs = algorithmComparison.autosensInput!
+
+        // change these variables to switch between 24 and 8 hours
+        // 288 for 24 hours, 96 for 8 hours
+        let maxDeviations = 288
+        // OpenAPSFixed.prepare24 and OpenAPSFixed.prepare8
+        let prepareFile = OpenAPSFixed.prepare24
+
+        let encoder = JSONCoding.encoder
+        let output = try encoder.encode(autosensInputs)
+
+        let sharedDir = FileManager.default.temporaryDirectory
+        let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
+        try output.write(to: outputURL)
+
+        // Print the path so you can find it
+        print("Writing to: \(outputURL.path)")
+
+        timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+        let openAps = OpenAPSFixed()
+
+        let glucose = try JSONBridge.glucose(from: autosensInputs.glucose)
+        let pumpHistory = try JSONBridge.pumpHistory(from: autosensInputs.history)
+        let basalProfile = try JSONBridge.basalProfile(from: autosensInputs.basalProfile)
+        let profile = autosensInputs.profile
+        let carbs = try JSONBridge.carbs(from: autosensInputs.carbs)
+        let tempTargets = try JSONBridge.tempTargets(from: autosensInputs.tempTargets)
+        let clock = autosensInputs.clock
+
+        let autosensResultSwift = try AutosensGenerator.generate(
+            glucose: glucose,
+            pumpHistory: pumpHistory,
+            basalProfile: basalProfile,
+            profile: profile,
+            carbs: carbs,
+            tempTargets: tempTargets,
+            maxDeviations: maxDeviations,
+            clock: clock,
+            includeDeviationsForTesting: true
+        )
+
+        let autosensResultJavascript = await openAps.autosenseJavascript(
+            glucose: autosensInputs.glucose,
+            pumpHistory: autosensInputs.history,
+            basalprofile: autosensInputs.basalProfile,
+            profile: try JSONBridge.to(autosensInputs.profile),
+            carbs: autosensInputs.carbs,
+            temptargets: autosensInputs.tempTargets,
+            clock: autosensInputs.clock,
+            prepareFile: prepareFile
+        )
+
+        if case let .success(jsJson) = autosensResultJavascript {
+            try compareDeviations(swiftJson: JSONBridge.to(autosensResultSwift), jsJson: jsJson)
+        }
+
+        try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
+
+        timeZoneForTests.resetTimezone()
+    }
 }

+ 15 - 11
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -37,6 +37,18 @@ import Testing
         }
     }
 
+    static func pumpIsSuspended(history: [PumpHistoryEvent]) -> Bool {
+        // The JS implementation of IoB when the pump is suspend is so fundamentally
+        // broken that I wasn't able to fix it in JS. So we'll just skip these, but I
+        // verified them by hand and the Swift implementation appears to be correct
+        if let mostRecentSuspendResumeEvent = history.filter({ $0.type == .pumpSuspend || $0.type == .pumpResume })
+            .first
+        {
+            return mostRecentSuspendResumeEvent.type == .pumpSuspend
+        }
+        return false
+    }
+
     // Note: This test case has a memory leak so limit your inputs
     // to about 250 files at a time
     @Test(
@@ -62,17 +74,9 @@ import Testing
                 continue
             }
 
-            // The JS implementation of IoB when the pump is suspend is so fundamentally
-            // broken that I wasn't able to fix it in JS. So we'll just skip these, but I
-            // verified them by hand and the Swift implementation appears to be correct
-            if let mostRecentSuspendResumeEvent = iobInputs.history.filter({ $0.type == .pumpSuspend || $0.type == .pumpResume })
-                .first
-            {
-                if mostRecentSuspendResumeEvent.type == .pumpSuspend
-                {
-                    print("Skipping, known issue with JS and currently suspended pumps")
-                    continue
-                }
+            if IobJsonTests.pumpIsSuspended(history: iobInputs.history) {
+                print("Skipping, known issue with JS and currently suspended pumps")
+                continue
             }
 
             timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)

+ 32 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens-prepare-24.js

@@ -0,0 +1,32 @@
+// для settings/autosens.json параметры: monitor/glucose.json monitor/pumphistory-24h-zoned.json settings/basal_profile.json settings/profile.json monitor/carbhistory.json settings/temptargets.json
+
+function generate(glucose_data, pumphistory_data, basalprofile, profile_data, carb_data = {}, temptarget_data = {}, now = null) {
+    if (glucose_data.length < 72) {
+        return { "ratio": 1, "error": "not enough glucose data to calculate autosens" };
+    };
+    
+    if (now) {
+        now = new Date(now);
+    } else {
+        now = new Date();
+    }
+    
+    var iob_inputs = {
+        history: pumphistory_data,
+        profile: profile_data,
+        clock: now
+    };
+
+    var detection_inputs = {
+        iob_inputs: iob_inputs,
+        carbs: carb_data,
+        glucose_data: glucose_data,
+        basalprofile: basalprofile,
+        temptargets: temptarget_data
+    };
+    
+    // 24 hours only
+    detection_inputs.deviations = 288;
+    return trio_autosens(detection_inputs, now);
+}
+

+ 32 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens-prepare-8.js

@@ -0,0 +1,32 @@
+// для settings/autosens.json параметры: monitor/glucose.json monitor/pumphistory-24h-zoned.json settings/basal_profile.json settings/profile.json monitor/carbhistory.json settings/temptargets.json
+
+function generate(glucose_data, pumphistory_data, basalprofile, profile_data, carb_data = {}, temptarget_data = {}, now = null) {
+    if (glucose_data.length < 72) {
+        return { "ratio": 1, "error": "not enough glucose data to calculate autosens" };
+    };
+    
+    if (now) {
+        now = new Date(now);
+    } else {
+        now = new Date();
+    }
+    
+    var iob_inputs = {
+        history: pumphistory_data,
+        profile: profile_data,
+        clock: now
+    };
+
+    var detection_inputs = {
+        iob_inputs: iob_inputs,
+        carbs: carb_data,
+        glucose_data: glucose_data,
+        basalprofile: basalprofile,
+        temptargets: temptarget_data
+    };
+    
+    // 8 hours only
+    detection_inputs.deviations = 96;
+    return trio_autosens(detection_inputs, now);
+}
+

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-prep.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-history.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/meal.js


+ 7 - 2
TrioTests/OpenAPSSwiftTests/utils/OpenAPSFixed.swift

@@ -143,6 +143,10 @@ final class OpenAPSFixed {
         }
     }
 
+    static let prepare = "autosens-prepare.js"
+    static let prepare24 = "autosens-prepare-24.js"
+    static let prepare8 = "autosens-prepare-8.js"
+
     func autosenseJavascript(
         glucose: JSON,
         pumpHistory: JSON,
@@ -150,7 +154,8 @@ final class OpenAPSFixed {
         profile: JSON,
         carbs: JSON,
         temptargets: JSON,
-        clock: JSON
+        clock: JSON,
+        prepareFile: String
     ) async -> OrefFunctionResult {
         do {
             let result = try await withCheckedThrowingContinuation { continuation in
@@ -160,7 +165,7 @@ final class OpenAPSFixed {
                     worker.evaluateBatch(scripts: [
                         Script(name: "prepare/log.js"),
                         Script.fromTestingBundle(name: "autosens.js", bundle: testBundle),
-                        Script.fromTestingBundle(name: "autosens-prepare.js", bundle: testBundle)
+                        Script.fromTestingBundle(name: prepareFile, bundle: testBundle)
                     ])
                     let result = worker.call(function: "generate", with: [
                         glucose,