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

Integrate the autosens algorithm and log differences

This PR integrates the swift implementation for autosens into the
app. It is running but we don't use the results, it just for
comparison for now.

It also includes some bug fixes discovered while testing around
rounding differences between Javascript and Swift.
Sam King 11 месяцев назад
Родитель
Сommit
42dd7c461a

+ 8 - 0
Trio.xcodeproj/project.pbxproj

@@ -203,6 +203,7 @@
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		3B0B4E6C2DE1439F005C6627 /* LockedResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */; };
 		3B139EF32DF06CE100D40797 /* AutosensGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B139EF22DF06CDC00D40797 /* AutosensGenerator.swift */; };
+		3B16C39C2DF75BD500C5C801 /* autosens-prepare.js in Resources */ = {isa = PBXBuildFile; fileRef = 3B16C39B2DF75BCB00C5C801 /* autosens-prepare.js */; };
 		3B1C5C292D68E1E3004E9273 /* IobCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C242D68E1E3004E9273 /* IobCalculation.swift */; };
 		3B1C5C2A2D68E1E3004E9273 /* IobGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C262D68E1E3004E9273 /* IobGenerator.swift */; };
 		3B1C5C2B2D68E1E3004E9273 /* IobHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C272D68E1E3004E9273 /* IobHistory.swift */; };
@@ -298,6 +299,7 @@
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		3BBB76AA2E01C70B0040977D /* MealCob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBB76A92E01C7070040977D /* MealCob.swift */; };
 		3BBC22632DF5B94100169236 /* AutosensTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBC22622DF5B93900169236 /* AutosensTests.swift */; };
+		3BBC227C2DF6F87200169236 /* HttpFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBC227B2DF6F86700169236 /* HttpFiles.swift */; };
 		3BC0AA3B2DA74C87000DF7B7 /* iob-total.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */; };
 		3BC0AA3E2DA817EC000DF7B7 /* iob-calculate.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3C2DA817EC000DF7B7 /* iob-calculate.js */; };
 		3BC0AA3F2DA817EC000DF7B7 /* iob-history.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3D2DA817EC000DF7B7 /* iob-history.js */; };
@@ -1120,6 +1122,7 @@
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedResolver.swift; sourceTree = "<group>"; };
 		3B139EF22DF06CDC00D40797 /* AutosensGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensGenerator.swift; sourceTree = "<group>"; };
+		3B16C39B2DF75BCB00C5C801 /* autosens-prepare.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "autosens-prepare.js"; sourceTree = "<group>"; };
 		3B1C5C242D68E1E3004E9273 /* IobCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobCalculation.swift; sourceTree = "<group>"; };
 		3B1C5C252D68E1E3004E9273 /* IobError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobError.swift; sourceTree = "<group>"; };
 		3B1C5C262D68E1E3004E9273 /* IobGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobGenerator.swift; sourceTree = "<group>"; };
@@ -1193,6 +1196,7 @@
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BBB76A92E01C7070040977D /* MealCob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealCob.swift; sourceTree = "<group>"; };
 		3BBC22622DF5B93900169236 /* AutosensTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensTests.swift; sourceTree = "<group>"; };
+		3BBC227B2DF6F86700169236 /* HttpFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpFiles.swift; sourceTree = "<group>"; };
 		3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-total.js"; sourceTree = "<group>"; };
 		3BC0AA3C2DA817EC000DF7B7 /* iob-calculate.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-calculate.js"; sourceTree = "<group>"; };
 		3BC0AA3D2DA817EC000DF7B7 /* iob-history.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-history.js"; sourceTree = "<group>"; };
@@ -2781,6 +2785,7 @@
 		3B1C5C3F2D68E269004E9273 /* utils */ = {
 			isa = PBXGroup;
 			children = (
+				3BBC227B2DF6F86700169236 /* HttpFiles.swift */,
 				3B1C5C3D2D68E269004E9273 /* Extensions.swift */,
 				3B4821812E080CAE00F0DD17 /* HttpFiles.swift */,
 				3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */,
@@ -2926,6 +2931,7 @@
 		3BF92F2B2D86DEE9006B545A /* bundle */ = {
 			isa = PBXGroup;
 			children = (
+				3B16C39B2DF75BCB00C5C801 /* autosens-prepare.js */,
 				3BC0AA402DA8B8F7000DF7B7 /* iob-history-prepare.js */,
 				3BF92F212D86DEE9006B545A /* autosens.js */,
 				3BF92F222D86DEE9006B545A /* autotune-core.js */,
@@ -4342,6 +4348,7 @@
 				3B8B5D402DF52D0E00365ED3 /* deviationsUnsorted.json in Resources */,
 				DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */,
 				3BCA5F7C2DC7B16400A7EAC7 /* pumphistory-with-external.json in Resources */,
+				3B16C39C2DF75BD500C5C801 /* autosens-prepare.js in Resources */,
 				3B8B5D332DF5238000365ED3 /* as-profile.json in Resources */,
 				3B8B5D342DF5238000365ED3 /* as-glucose.json in Resources */,
 				3B8B5D352DF5238000365ED3 /* as-pump.json in Resources */,
@@ -5076,6 +5083,7 @@
 				3B8B5D3E2DF5240C00365ED3 /* TimeZoneForTests.swift in Sources */,
 				3B5CD2CA2D4AECD500CE213C /* ProfileJavascriptTests.swift in Sources */,
 				3B5CD2CB2D4AECD500CE213C /* ProfileTargetsTests.swift in Sources */,
+				3BBC227C2DF6F87200169236 /* HttpFiles.swift in Sources */,
 				3B5CD2CD2D4AECD500CE213C /* ProfileIsfTests.swift in Sources */,
 				3B5CD2CE2D4AECD500CE213C /* ProfileBasalTests.swift in Sources */,
 				3BFA5BF92D989F510072B082 /* MockTDDStorage.swift in Sources */,

+ 1 - 1
Trio/Sources/APS/APSManager.swift

@@ -387,7 +387,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard let autosense = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
               (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
         else {
-            let result = try await openAPS.autosense()
+            let result = try await openAPS.autosense(useSwiftOref: settings.useSwiftOref)
             return result != nil
         }
 

+ 76 - 19
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -465,7 +465,7 @@ final class OpenAPS {
         }
     }
 
-    func autosense() async throws -> Autosens? {
+    func autosense(useSwiftOref: Bool) async throws -> Autosens? {
         debug(.openAPS, "Start autosens")
 
         // Perform asynchronous calls in parallel
@@ -493,7 +493,8 @@ final class OpenAPS {
             basalprofile: basalProfile,
             profile: profile,
             carbs: carbsAsJSON,
-            temptargets: tempTargets
+            temptargets: tempTargets,
+            useSwiftOref: useSwiftOref
         )
 
         debug(.openAPS, "AUTOSENS: \(autosenseResult)")
@@ -743,25 +744,81 @@ final class OpenAPS {
         basalprofile: JSON,
         profile: JSON,
         carbs: JSON,
-        temptargets: JSON
+        temptargets: JSON,
+        useSwiftOref: Bool
     ) async throws -> RawJSON {
-        try await withCheckedThrowingContinuation { continuation in
-            jsWorker.inCommonContext { worker in
-                worker.evaluateBatch(scripts: [
-                    Script(name: Prepare.log),
-                    Script(name: Bundle.autosens),
-                    Script(name: Prepare.autosens)
-                ])
-                let result = worker.call(function: Function.generate, with: [
-                    glucose,
-                    pumpHistory,
-                    basalprofile,
-                    profile,
-                    carbs,
-                    temptargets
-                ])
-                continuation.resume(returning: result)
+        let startJavascriptAt = Date()
+        let jsResult = await autosenseJavascript(
+            glucose: glucose,
+            pumpHistory: pumpHistory,
+            basalprofile: basalprofile,
+            profile: profile,
+            carbs: carbs,
+            temptargets: temptargets
+        )
+        let javascriptDuration = Date().timeIntervalSince(startJavascriptAt)
+
+        // Important: we want to make sure that this flag ensures that none
+        // of the native code runs
+        guard useSwiftOref else {
+            return try jsResult.returnOrThrow()
+        }
+
+        let startSwiftAt = Date()
+        let (swiftResult, autosensInputs) = OpenAPSSwift
+            .autosense(
+                glucose: glucose,
+                pumpHistory: pumpHistory,
+                basalProfile: basalprofile,
+                profile: profile,
+                carbs: carbs,
+                tempTargets: temptargets,
+                clock: Date()
+            )
+        let swiftDuration = Date().timeIntervalSince(startSwiftAt)
+
+        JSONCompare.logDifferences(
+            function: .autosens,
+            swift: swiftResult,
+            swiftDuration: swiftDuration,
+            javascript: jsResult,
+            javascriptDuration: javascriptDuration,
+            autosensInputs: autosensInputs
+        )
+
+        return try jsResult.returnOrThrow()
+    }
+
+    private func autosenseJavascript(
+        glucose: JSON,
+        pumpHistory: JSON,
+        basalprofile: JSON,
+        profile: JSON,
+        carbs: JSON,
+        temptargets: JSON
+    ) async -> OrefFunctionResult {
+        do {
+            let result = try await withCheckedThrowingContinuation { continuation in
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: Prepare.log),
+                        Script(name: Bundle.autosens),
+                        Script(name: Prepare.autosens)
+                    ])
+                    let result = worker.call(function: Function.generate, with: [
+                        glucose,
+                        pumpHistory,
+                        basalprofile,
+                        profile,
+                        carbs,
+                        temptargets
+                    ])
+                    continuation.resume(returning: result)
+                }
             }
+            return .success(result)
+        } catch {
+            return .failure(error)
         }
     }
 

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

@@ -76,7 +76,9 @@ struct AutosensGenerator {
             simulationProfile.currentBasal = try Basal.basalLookup(basalProfile, now: currGlucose.date)
             simulationProfile.temptargetSet = false
             let iob = try IobCalculation.iobTotal(treatments: treatments, profile: simulationProfile, time: currGlucose.date)
-            let bgi = (-iob.activity * sensitivity * 5).rounded(scale: 2)
+
+            // copying Javascript rounding
+            let bgi = (-iob.activity * sensitivity * 5 * 100 + 0.5).rounded(scale: 0, roundingMode: .down) / 100
 
             // BUG: the time span for deltaGlucose might be different
             // then the time span for bgi if there was a missing CGM

+ 14 - 0
Trio/Sources/APS/OpenAPSSwift/Logging/AlgorithmComparison.swift

@@ -81,6 +81,17 @@ struct MealInputs: Codable {
     let glucose: [BloodGlucose]
 }
 
+/// For tracking inputs to Autosens when there is a mismatch
+struct AutosensInputs: Codable {
+    let glucose: [BloodGlucose]
+    let history: [PumpHistoryEvent]
+    let basalProfile: [BasalProfileEntry]
+    let profile: Profile
+    let carbs: [CarbsEntry]
+    let tempTargets: [TempTarget]
+    let clock: Date
+}
+
 /// Represents a complete comparison between JS and Swift implementations
 struct AlgorithmComparison: Codable {
     let id: UUID
@@ -107,6 +118,7 @@ struct AlgorithmComparison: Codable {
     // Inputs for mismatches
     let iobInput: IobInputs?
     let mealInput: MealInputs?
+    let autosensInput: AutosensInputs?
 
     init(
         function: OrefFunction,
@@ -119,6 +131,7 @@ struct AlgorithmComparison: Codable {
         comparisonError: AlgorithmException? = nil,
         iobInputs: IobInputs? = nil,
         mealInputs: MealInputs? = nil,
+        autosensInputs: AutosensInputs? = nil,
         id: UUID = UUID(),
         createdAt: Date = Date()
     ) {
@@ -134,6 +147,7 @@ struct AlgorithmComparison: Codable {
         self.comparisonError = comparisonError
         iobInput = iobInputs
         mealInput = mealInputs
+        autosensInput = autosensInputs
         timezone = TimeZone.current.identifier
         version = "3"
 

+ 12 - 6
Trio/Sources/APS/OpenAPSSwift/Logging/JSONCompare.swift

@@ -85,7 +85,8 @@ enum JSONCompare {
         javascript: OrefFunctionResult,
         javascriptDuration: TimeInterval,
         iobInputs: IobInputs? = nil,
-        mealInputs: MealInputs? = nil
+        mealInputs: MealInputs? = nil,
+        autosensInputs: AutosensInputs? = nil
     ) {
         let comparison = createComparison(
             function: function,
@@ -94,7 +95,8 @@ enum JSONCompare {
             javascript: javascript,
             javascriptDuration: javascriptDuration,
             iobInputs: iobInputs,
-            mealInputs: mealInputs
+            mealInputs: mealInputs,
+            autosensInputs: autosensInputs
         )
 
         Task {
@@ -113,7 +115,8 @@ enum JSONCompare {
         javascript: OrefFunctionResult,
         javascriptDuration: TimeInterval,
         iobInputs: IobInputs?,
-        mealInputs: MealInputs?
+        mealInputs: MealInputs?,
+        autosensInputs: AutosensInputs?
     ) -> AlgorithmComparison {
         switch (swift, javascript) {
         case let (.success(swiftJson), .success(javascriptJson)):
@@ -127,7 +130,8 @@ enum JSONCompare {
                     swiftDuration: swiftDuration,
                     differences: differences.isEmpty ? nil : differences,
                     iobInputs: differences.isEmpty ? nil : iobInputs,
-                    mealInputs: differences.isEmpty ? nil : mealInputs
+                    mealInputs: differences.isEmpty ? nil : mealInputs,
+                    autosensInputs: differences.isEmpty ? nil : autosensInputs
                 )
             } catch {
                 return AlgorithmComparison(
@@ -154,7 +158,8 @@ enum JSONCompare {
                 jsDuration: javascriptDuration,
                 swiftException: AlgorithmException(error: swiftError),
                 iobInputs: iobInputs,
-                mealInputs: mealInputs
+                mealInputs: mealInputs,
+                autosensInputs: autosensInputs
             )
 
         case let (.success, .failure(jsError)):
@@ -164,7 +169,8 @@ enum JSONCompare {
                 swiftDuration: swiftDuration,
                 jsException: AlgorithmException(error: jsError),
                 iobInputs: iobInputs,
-                mealInputs: mealInputs
+                mealInputs: mealInputs,
+                autosensInputs: autosensInputs
             )
         }
     }

+ 11 - 1
Trio/Sources/APS/OpenAPSSwift/Logging/OrefFunction.swift

@@ -21,9 +21,10 @@ enum OrefFunction: String, Codable {
         case dictionary
     }
 
-    case makeProfile
+    case autosens
     case iob
     case meal
+    case makeProfile
 
     // since we're removing some keys from our Profile that exist in Javascript
     // we need to let the difference function know which keys to ignore when
@@ -39,6 +40,8 @@ enum OrefFunction: String, Codable {
             // These aren't used by downstream calculations, so we
             // can ignore them in our comparison
             return Set(["maxDeviation", "minDeviation", "allDeviations", "bwCarbs", "bwFound", "journalCarbs", "nsCarbs"])
+        case .autosens:
+            return Set()
         }
     }
 
@@ -71,6 +74,11 @@ enum OrefFunction: String, Codable {
                 "slopeFromMinDeviation": 0.25,
                 "lastCarbTime": 1
             ]
+        case .autosens:
+            return [
+                "ratio": 0.01,
+                "newisf": 1
+            ]
         }
     }
 
@@ -82,6 +90,8 @@ enum OrefFunction: String, Codable {
             return .array
         case .meal:
             return .dictionary
+        case .autosens:
+            return .dictionary
         }
     }
 }

+ 61 - 8
Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift

@@ -108,13 +108,66 @@ struct OpenAPSSwift {
     }
 
     static func autosense(
-        glucose _: JSON,
-        pumpHistory _: JSON,
-        basalprofile _: JSON,
-        profile _: JSON,
-        carbs _: JSON,
-        temptargets _: JSON
-    ) -> OrefFunctionResult {
-        .failure(NSError(domain: "Some error", code: 1, userInfo: nil))
+        glucose: JSON,
+        pumpHistory: JSON,
+        basalProfile: JSON,
+        profile: JSON,
+        carbs: JSON,
+        tempTargets: JSON,
+        clock: JSON,
+        includeDeviationsForTesting: Bool = false
+    ) -> (OrefFunctionResult, AutosensInputs?) {
+        var autosensInputs: AutosensInputs?
+
+        do {
+            let glucose = try JSONBridge.glucose(from: glucose)
+            let pumpHistory = try JSONBridge.pumpHistory(from: pumpHistory)
+            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
+            let profile = try JSONBridge.profile(from: profile)
+            let carbs = try JSONBridge.carbs(from: carbs)
+            let tempTargets = try JSONBridge.tempTargets(from: tempTargets)
+            let clock = try JSONBridge.clock(from: clock)
+
+            autosensInputs = AutosensInputs(
+                glucose: glucose,
+                history: pumpHistory,
+                basalProfile: basalProfile,
+                profile: profile,
+                carbs: carbs,
+                tempTargets: tempTargets,
+                clock: clock
+            )
+
+            // this logic is from prepare/autosens.js
+            let ratio8h = try AutosensGenerator.generate(
+                glucose: glucose,
+                pumpHistory: pumpHistory,
+                basalProfile: basalProfile,
+                profile: profile,
+                carbs: carbs,
+                tempTargets: tempTargets,
+                maxDeviations: 96,
+                clock: clock,
+                includeDeviationsForTesting: includeDeviationsForTesting
+            )
+
+            let ratio24h = try AutosensGenerator.generate(
+                glucose: glucose,
+                pumpHistory: pumpHistory,
+                basalProfile: basalProfile,
+                profile: profile,
+                carbs: carbs,
+                tempTargets: tempTargets,
+                maxDeviations: 288,
+                clock: clock,
+                includeDeviationsForTesting: includeDeviationsForTesting
+            )
+
+            let lowestRatio = ratio8h.ratio < ratio24h.ratio ? ratio8h : ratio24h
+
+            return try (.success(JSONBridge.to(lowestRatio)), autosensInputs)
+        } catch {
+            return (.failure(error), autosensInputs)
+        }
     }
 }

+ 141 - 0
TrioTests/OpenAPSSwiftTests/AutosensJsonTests.swift

@@ -51,4 +51,145 @@ import Testing
 
         timeZoneForTests.resetTimezone()
     }
+
+    func checkFixedJsAgainstSwift(autosensInputs: AutosensInputs) async throws {
+        let openAps = OpenAPSFixed()
+        let (autosensResultSwift, _) = OpenAPSSwift.autosense(
+            glucose: autosensInputs.glucose,
+            pumpHistory: autosensInputs.history,
+            basalProfile: autosensInputs.basalProfile,
+            profile: try JSONBridge.to(autosensInputs.profile),
+            carbs: autosensInputs.carbs,
+            tempTargets: autosensInputs.tempTargets,
+            clock: autosensInputs.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
+        )
+
+        let comparison = JSONCompare.createComparison(
+            function: .autosens,
+            swift: autosensResultSwift,
+            swiftDuration: 0.1,
+            javascript: autosensResultJavascript,
+            javascriptDuration: 0.1,
+            iobInputs: nil,
+            mealInputs: nil,
+            autosensInputs: nil
+        )
+
+        if comparison.resultType == .valueDifference {
+            print(comparison.differences!.prettyPrintedJSON!)
+        }
+
+        if comparison.resultType != .matching {
+            print("REPLAY ERROR: Fixed JS didn't match")
+            if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
+                try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
+            }
+        }
+
+        #expect(comparison.resultType == .matching)
+    }
+
+    func compareDeviations(swiftJson: String, jsJson: String) throws {
+        // Parse both JSON strings
+        let swiftData = swiftJson.data(using: .utf8)!
+        let jsData = jsJson.data(using: .utf8)!
+
+        let swiftDict = try JSONSerialization.jsonObject(with: swiftData) as! [String: Any]
+        let jsDict = try JSONSerialization.jsonObject(with: jsData) as! [String: Any]
+
+        // Extract deviationsUnsorted arrays
+        let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
+        let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
+
+        // Convert both to Double arrays
+        let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
+            if let number = value as? NSNumber {
+                return number.doubleValue
+            }
+            return nil
+        }
+
+        let jsDoubles = jsDeviations.compactMap { value -> Double? in
+            if let number = value as? NSNumber {
+                return number.doubleValue
+            } else if let string = value as? String {
+                return Double(string)
+            }
+            return nil
+        }
+
+        // Compare the arrays
+        print("Swift array count: \(swiftDoubles.count)")
+        print("JS array count: \(jsDoubles.count)")
+
+        guard swiftDoubles.count == jsDoubles.count else {
+            print("Arrays have different lengths!")
+            print("Swift: \(swiftDoubles)")
+            print("JS: \(jsDoubles)")
+            return
+        }
+
+        var differences: [(index: Int, swift: Double, js: Double)] = []
+
+        for (index, (swiftVal, jsVal)) in zip(swiftDoubles, jsDoubles).enumerated() {
+            if abs(swiftVal - jsVal) > 0.001 { // Small tolerance for floating point comparison
+                differences.append((index: index, swift: swiftVal, js: jsVal))
+            }
+        }
+
+        if differences.isEmpty {
+            print("✅ Arrays are identical (within tolerance)")
+        } else {
+            print("❌ Found \(differences.count) differences:")
+            for diff in differences {
+                print("  Index \(diff.index): Swift=\(diff.swift), JS=\(diff.js)")
+            }
+        }
+    }
+
+    @Test(
+        "should produce same results for autosens for fixed JS",
+        .enabled(if: false)
+    ) func replayErrorInputs() async throws {
+        let files = try await HttpFiles.listFiles()
+        for filePath in files {
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
+            print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
+            guard let autosensInputs = algorithmComparison.autosensInput else {
+                print("Skipping, no autosensInputs found")
+                if let str = algorithmComparison.comparisonError {
+                    print(str)
+                }
+                if let str = algorithmComparison.swiftException {
+                    print(str)
+                }
+                continue
+            }
+
+            // remove this
+            let encoder = JSONCoding.encoder
+            var output = try encoder.encode(autosensInputs)
+            var sharedDir = FileManager.default.temporaryDirectory
+            var outputURL = sharedDir.appendingPathComponent("autosens_inputs.json")
+            print("Writing to: \(outputURL.path)")
+            try output.write(to: outputURL)
+
+            timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+            try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
+
+            timeZoneForTests.resetTimezone()
+        }
+    }
 }

+ 4 - 2
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -90,7 +90,8 @@ import Testing
             javascript: iobResultJavascript,
             javascriptDuration: 0.1,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         if comparison.resultType == .valueDifference {
@@ -127,7 +128,8 @@ import Testing
             javascript: iobResultJavascript,
             javascriptDuration: 0.1,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         if comparison.resultType != .valueDifference {

+ 2 - 1
TrioTests/OpenAPSSwiftTests/MealJsonTests.swift

@@ -78,7 +78,8 @@ import Testing
             javascript: mealResultJavascript,
             javascriptDuration: 0.1,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         if comparison.resultType == .valueDifference {

+ 2 - 1
TrioTests/OpenAPSSwiftTests/ProfileJavascriptTests.swift

@@ -328,7 +328,8 @@ struct ProfileGeneratorTests {
             javascript: jsResult,
             javascriptDuration: 1.0,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         if comparison.resultType == .valueDifference {

+ 14 - 7
TrioTests/OpenAPSSwiftTests/ProfileJsNativeCompareTests.swift

@@ -96,7 +96,8 @@ import Testing
             javascript: profileJs,
             javascriptDuration: 0.1,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -127,7 +128,8 @@ import Testing
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -147,7 +149,8 @@ import Testing
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .valueDifference)
@@ -169,7 +172,8 @@ import Testing
             javascript: .failure(error),
             javascriptDuration: 0.2,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .matchingExceptions)
@@ -188,7 +192,8 @@ import Testing
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .swiftOnlyException)
@@ -209,7 +214,8 @@ import Testing
             javascript: .failure(error),
             javascriptDuration: 0.2,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .jsOnlyException)
@@ -229,7 +235,8 @@ import Testing
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
             iobInputs: nil,
-            mealInputs: nil
+            mealInputs: nil,
+            autosensInputs: nil
         )
 
         #expect(comparison.resultType == .comparisonError)

+ 32 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens-prepare.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
+    };
+
+    var detection_inputs = {
+        iob_inputs: iob_inputs,
+        carbs: carb_data,
+        glucose_data: glucose_data,
+        basalprofile: basalprofile,
+        temptargets: temptarget_data
+    };
+    detection_inputs.deviations = 96;
+    var ratio8h = trio_autosens(detection_inputs, now);
+    detection_inputs.deviations = 288;
+    var ratio24h = trio_autosens(detection_inputs, now);
+    var lowestRatio = ratio8h.ratio < ratio24h.ratio ? ratio8h : ratio24h;
+    return lowestRatio;
+}

+ 51 - 0
TrioTests/OpenAPSSwiftTests/utils/OpenAPSFixed.swift

@@ -73,6 +73,42 @@ final class OpenAPSFixed {
         }
     }
 
+    func autosenseJavascript(
+        glucose: JSON,
+        pumpHistory: JSON,
+        basalprofile: JSON,
+        profile: JSON,
+        carbs: JSON,
+        temptargets: JSON,
+        clock: JSON
+    ) async -> OrefFunctionResult {
+        do {
+            let result = try await withCheckedThrowingContinuation { continuation in
+                let testBundle = Bundle(for: OpenAPSFixed.self)
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: "prepare/log.js"),
+                        Script.fromTestingBundle(name: "autosens.js", bundle: testBundle),
+                        Script.fromTestingBundle(name: "autosens-prepare.js", bundle: testBundle)
+                    ])
+                    let result = worker.call(function: "generate", with: [
+                        glucose,
+                        pumpHistory,
+                        basalprofile,
+                        profile,
+                        carbs,
+                        temptargets,
+                        clock
+                    ])
+                    continuation.resume(returning: result)
+                }
+            }
+            return .success(result)
+        } catch {
+            return .failure(error)
+        }
+    }
+
     func iobJavascript(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async -> OrefFunctionResult {
         do {
             let testBundle = Bundle(for: OpenAPSFixed.self)
@@ -112,8 +148,23 @@ extension Script {
             }
         } else {
             print("Resource not found: javascript/\(name)")
+            testPrintAllJSFiles(testBundle: bundle)
             body = "Resource not found"
         }
         return Script(name: name, body: body)
     }
+
+    static func testPrintAllJSFiles(testBundle: Bundle) {
+        // Get all .js files in the bundle
+        if let jsURLs = testBundle.urls(forResourcesWithExtension: "js", subdirectory: nil) {
+            print("JavaScript files in test bundle:")
+            for jsURL in jsURLs {
+                print("- \(jsURL.lastPathComponent)")
+                print("  Full path: \(jsURL.path)")
+            }
+            print("Total JS files found: \(jsURLs.count)")
+        } else {
+            print("No JavaScript files found in test bundle")
+        }
+    }
 }