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

Merge branch 'oref-swift' of github.com:nightscout/Trio-dev into determine-basal-to-swift-oref

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

+ 10 - 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 */; };
@@ -1135,6 +1137,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>"; };
@@ -1208,6 +1211,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>"; };
@@ -2811,6 +2815,7 @@
 		3B1C5C3F2D68E269004E9273 /* utils */ = {
 			isa = PBXGroup;
 			children = (
+				3BBC227B2DF6F86700169236 /* HttpFiles.swift */,
 				3B1C5C3D2D68E269004E9273 /* Extensions.swift */,
 				3B4821812E080CAE00F0DD17 /* HttpFiles.swift */,
 				3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */,
@@ -2870,8 +2875,10 @@
 				3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */,
 				3B5CD2A32D4AEA5D00CE213C /* Date+MinutesFromMidnight.swift */,
 				3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */,
+				3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */,
 				3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */,
 				3B1C5C312D68E233004E9273 /* PumpHistory+copy.swift */,
+				3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */,
 				3B1C5C322D68E233004E9273 /* TimeExtensions.swift */,
 			);
 			path = Extensions;
@@ -2966,6 +2973,7 @@
 		3BF92F2B2D86DEE9006B545A /* bundle */ = {
 			isa = PBXGroup;
 			children = (
+				3B16C39B2DF75BCB00C5C801 /* autosens-prepare.js */,
 				3BC0AA402DA8B8F7000DF7B7 /* iob-history-prepare.js */,
 				3BF92F212D86DEE9006B545A /* autosens.js */,
 				3BF92F222D86DEE9006B545A /* autotune-core.js */,
@@ -4403,6 +4411,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 */,
@@ -5150,6 +5159,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 - 172
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,25 +1,7 @@
 {
-  "originHash" : "89074a88ed67a58ecd7534519854c5a0928a4046d7c8a6123a7d70f27bf8b44d",
+  "originHash" : "e86ddc0787a4009654195f670305dba355987194a6030935dd73ee0db8da3359",
   "pins" : [
     {
-      "identity" : "abseil-cpp-binary",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/abseil-cpp-binary.git",
-      "state" : {
-        "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
-        "version" : "1.2024072200.0"
-      }
-    },
-    {
-      "identity" : "app-check",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/app-check.git",
-      "state" : {
-        "revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
-        "version" : "11.2.0"
-      }
-    },
-    {
       "identity" : "cryptoswift",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/krzyzanowskim/CryptoSwift",
@@ -29,105 +11,6 @@
       }
     },
     {
-      "identity" : "firebase-ios-sdk",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/firebase/firebase-ios-sdk.git",
-      "state" : {
-        "revision" : "d1f7c7e8eaa74d7e44467184dc5f592268247d33",
-        "version" : "11.11.0"
-      }
-    },
-    {
-      "identity" : "googleappmeasurement",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/GoogleAppMeasurement.git",
-      "state" : {
-        "revision" : "dd89fc79a77183830742a16866d87e4e54785734",
-        "version" : "11.11.0"
-      }
-    },
-    {
-      "identity" : "googledatatransport",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/GoogleDataTransport.git",
-      "state" : {
-        "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
-        "version" : "10.1.0"
-      }
-    },
-    {
-      "identity" : "googleutilities",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/GoogleUtilities.git",
-      "state" : {
-        "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
-        "version" : "8.0.2"
-      }
-    },
-    {
-      "identity" : "grpc-binary",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/grpc-binary.git",
-      "state" : {
-        "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
-        "version" : "1.69.0"
-      }
-    },
-    {
-      "identity" : "gtm-session-fetcher",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/gtm-session-fetcher.git",
-      "state" : {
-        "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
-        "version" : "4.4.0"
-      }
-    },
-    {
-      "identity" : "interop-ios-for-google-sdks",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
-      "state" : {
-        "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
-        "version" : "101.0.0"
-      }
-    },
-    {
-      "identity" : "leveldb",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/firebase/leveldb.git",
-      "state" : {
-        "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
-        "version" : "1.22.5"
-      }
-    },
-    {
-      "identity" : "mkringprogressview",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
-      "state" : {
-        "branch" : "master",
-        "revision" : "660888aab1d2ab0ed7eb9eb53caec12af4955fa7"
-      }
-    },
-    {
-      "identity" : "nanopb",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/firebase/nanopb.git",
-      "state" : {
-        "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
-        "version" : "2.30910.0"
-      }
-    },
-    {
-      "identity" : "promises",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/google/promises.git",
-      "state" : {
-        "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
-        "version" : "2.4.0"
-      }
-    },
-    {
       "identity" : "slidebutton",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/no-comment/SlideButton",
@@ -137,33 +20,6 @@
       }
     },
     {
-      "identity" : "swift-algorithms",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/apple/swift-algorithms",
-      "state" : {
-        "revision" : "2327673b0e9c7e90e6b1826376526ec3627210e4",
-        "version" : "0.2.1"
-      }
-    },
-    {
-      "identity" : "swift-numerics",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/apple/swift-numerics",
-      "state" : {
-        "revision" : "6583ac70c326c3ee080c1d42d9ca3361dca816cd",
-        "version" : "0.1.0"
-      }
-    },
-    {
-      "identity" : "swift-protobuf",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/apple/swift-protobuf.git",
-      "state" : {
-        "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
-        "version" : "1.29.0"
-      }
-    },
-    {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
@@ -173,33 +29,6 @@
       }
     },
     {
-      "identity" : "swiftdate",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/malcommac/SwiftDate",
-      "state" : {
-        "revision" : "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
-        "version" : "6.3.1"
-      }
-    },
-    {
-      "identity" : "swiftmessages",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/SwiftKickMobile/SwiftMessages",
-      "state" : {
-        "revision" : "62e12e138fc3eedf88c7553dd5d98712aa119f40",
-        "version" : "9.0.9"
-      }
-    },
-    {
-      "identity" : "swinject",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/Swinject/Swinject",
-      "state" : {
-        "revision" : "be9dbcc7b86811bc131539a20c6f9c2d3e56919f",
-        "version" : "2.9.1"
-      }
-    },
-    {
       "identity" : "tidepoolkit",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/tidepool-org/TidepoolKit",

+ 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)
         }
     }
 

+ 14 - 12
Trio/Sources/APS/OpenAPSSwift/Autosens/AutosensGenerator.swift

@@ -16,7 +16,7 @@ struct AutosensGenerator {
             case nonMeal
         }
 
-        var meals: [CarbsEntry]
+        var meals: [MealInput]
         var absorbing = false
         var uam = false
         var mealCOB: Decimal = 0
@@ -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
@@ -250,10 +252,10 @@ struct AutosensGenerator {
 
         // BUG: This should be in a loop to handle more than one
         // carb entry (i.e., if entered close together in time)
-        if let meal = state.meals.last, meal.date < glucose.date {
-            if meal.carbs >= 1 {
-                state.mealCOB += meal.carbs
-                state.mealCarbs += meal.carbs
+        if let meal = state.meals.last, meal.timestamp < glucose.date {
+            if let carbs = meal.carbs, carbs >= 1 {
+                state.mealCOB += carbs
+                state.mealCarbs += carbs
             }
             state.meals = state.meals.dropLast()
         }
@@ -306,14 +308,14 @@ struct AutosensGenerator {
 
     /// Finds carbs and returns them in descending order, oldest records first
     private static func findMeals(
-        history _: [PumpHistoryEvent],
+        history: [PumpHistoryEvent],
         carbs: [CarbsEntry],
         profile _: Profile,
-        bucketedGlucose: [BucketedGlucose]
-    ) -> [CarbsEntry] {
-        let firstGlucoseDate = bucketedGlucose.first?.date ?? .distantPast
-        // TODO: Hook up to meal functions when they're ready
-        return carbs.sorted(by: { $0.date > $1.date }).filter({ $0.date >= firstGlucoseDate })
+        bucketedGlucose _: [BucketedGlucose]
+    ) -> [MealInput] {
+        let meals = MealHistory.findMealInputs(pumpHistory: history, carbHistory: carbs)
+
+        return meals.sorted(by: { $0.timestamp > $1.timestamp })
     }
 
     /// Find the last site change, falling back to 24 hours ago if not found

+ 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"
 

+ 18 - 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
             )
         }
     }
@@ -297,6 +303,12 @@ enum JSONCompare {
             return true
         case let (.string(s1), .string(s2)):
             return s1 == s2
+        case let (.string(s1), .number(n2)):
+            guard let n1 = Double(s1) else { return false }
+            return n1.isApproximatelyEqual(to: n2, epsilon: approximately)
+        case let (.number(n1), .string(s2)):
+            guard let n2 = Double(s2) else { return false }
+            return n1.isApproximatelyEqual(to: n2, epsilon: approximately)
         case let (.number(n1), .number(n2)):
             let match = n1.isApproximatelyEqual(to: n2, epsilon: approximately)
             return match

+ 12 - 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,12 @@ enum OrefFunction: String, Codable {
                 "slopeFromMinDeviation": 0.25,
                 "lastCarbTime": 1
             ]
+        case .autosens:
+            return [
+                "ratio": 0.01,
+                "newisf": 1,
+                "deviationsUnsorted": 0.02
+            ]
         }
     }
 
@@ -82,6 +91,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)
+        }
     }
 }

+ 190 - 0
TrioTests/OpenAPSSwiftTests/AutosensJsonTests.swift

@@ -51,4 +51,194 @@ 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")
+        }
+
+        #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 timezone = "America/Los_Angeles"
+        var skippedTimezones = Set<String>()
+        let files = try await HttpFiles.listFiles()
+        for filePath in files {
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
+            print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
+            guard timezone == algorithmComparison.timezone else {
+                skippedTimezones.insert(algorithmComparison.timezone)
+                continue
+            }
+            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
+            }
+
+            timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+            try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
+            print("Checked \(filePath) @ \(algorithmComparison.createdAt)")
+
+            timeZoneForTests.resetTimezone()
+        }
+
+        print("Skipped timezones:")
+        for skippedTimezone in skippedTimezones {
+            print("  - \(skippedTimezone)")
+        }
+    }
+
+    @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/4f38ce73-1526-4bcd-80d5-1dee5b002519.0.json")
+        let autosensInputs = algorithmComparison.autosensInput!
+
+        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)
+
+        timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+        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
+        )
+
+        if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
+            try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
+        }
+
+        // Print the path so you can find it
+        print("Writing to: \(outputURL.path)")
+
+        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 {

+ 3 - 2
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 {
@@ -92,7 +93,7 @@ import Testing
         #expect(comparison.resultType == .matching)
     }
 
-    @Test("Format inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
+    @Test("Format meal inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
         let openAps = OpenAPSFixed()
 
         // this test is meant for one-off analysis so it's ok to hard code

+ 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")
+        }
+    }
 }