Sfoglia il codice sorgente

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

Deniz Cengiz 11 mesi fa
parent
commit
f5ae87500b
31 ha cambiato i file con 426 aggiunte e 126 eliminazioni
  1. 4 4
      Trio.xcodeproj/project.pbxproj
  2. 74 18
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  3. 8 0
      Trio/Sources/APS/OpenAPSSwift/JSONBridge.swift
  4. 14 1
      Trio/Sources/APS/OpenAPSSwift/Logging/AlgorithmComparison.swift
  5. 12 6
      Trio/Sources/APS/OpenAPSSwift/Logging/JSONCompare.swift
  6. 16 0
      Trio/Sources/APS/OpenAPSSwift/Logging/OrefFunction.swift
  7. 7 5
      Trio/Sources/APS/OpenAPSSwift/Meal/MealCob.swift
  8. 1 3
      Trio/Sources/APS/OpenAPSSwift/Meal/MealGenerator.swift
  9. 5 2
      Trio/Sources/APS/OpenAPSSwift/Meal/MealTotal.swift
  10. 42 0
      Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift
  11. 7 11
      TrioTests/OpenAPSSwiftTests/IobJsonTests.swift
  12. 7 0
      TrioTests/OpenAPSSwiftTests/MealCobTests.swift
  13. 143 39
      TrioTests/OpenAPSSwiftTests/MealJsonTests.swift
  14. 1 1
      TrioTests/OpenAPSSwiftTests/MealTotalTests.swift
  15. 2 1
      TrioTests/OpenAPSSwiftTests/ProfileJavascriptTests.swift
  16. 14 7
      TrioTests/OpenAPSSwiftTests/ProfileJsNativeCompareTests.swift
  17. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens.js
  18. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-core.js
  19. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-prep.js
  20. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/basal-set-temp.js
  21. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/determine-basal.js
  22. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/glucose-get-last.js
  23. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-calculate.js
  24. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-history.js
  25. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-total.js
  26. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/iob.js
  27. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/meal.js
  28. 1 1
      TrioTests/OpenAPSSwiftTests/javascript/bundle/profile.js
  29. 0 16
      TrioTests/OpenAPSSwiftTests/json/meal-input-sim.json
  30. 23 0
      TrioTests/OpenAPSSwiftTests/utils/HttpFiles.swift
  31. 34 0
      TrioTests/OpenAPSSwiftTests/utils/OpenAPSFixed.swift

+ 4 - 4
Trio.xcodeproj/project.pbxproj

@@ -222,6 +222,7 @@
 		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
 		3B4550532D862C0000551B0D /* PumpHistoryEvent+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */; };
 		3B47C6102DA0A28F00B0E5EF /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */; };
+		3B4821822E080CAE00F0DD17 /* HttpFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4821812E080CAE00F0DD17 /* HttpFiles.swift */; };
 		3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; };
 		3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; };
@@ -317,7 +318,6 @@
 		3BEA3AE32D58F79700A67A1D /* JSONCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEA3ADC2D58F79700A67A1D /* JSONCompare.swift */; };
 		3BEF6AB12D9731660076089D /* MealHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB02D9731530076089D /* MealHistoryTests.swift */; };
 		3BEF6AB32D97316F0076089D /* MealTotalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB22D97316A0076089D /* MealTotalTests.swift */; };
-		3BEF6AB52D9750330076089D /* meal-input-sim.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB42D9750330076089D /* meal-input-sim.json */; };
 		3BEF6AB72D9750780076089D /* MealJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB62D9750710076089D /* MealJsonTests.swift */; };
 		3BF424C72DF4805A0017CFD9 /* AutosensError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF424C62DF480550017CFD9 /* AutosensError.swift */; };
 		3BF8D0C12D5175BE001B3F84 /* ProfileJsNativeCompareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF8D0C02D5175B3001B3F84 /* ProfileJsNativeCompareTests.swift */; };
@@ -1153,6 +1153,7 @@
 		3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
 		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
 		3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpHistoryEvent+Duplicates.swift"; sourceTree = "<group>"; };
+		3B4821812E080CAE00F0DD17 /* HttpFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpFiles.swift; sourceTree = "<group>"; };
 		3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DanaKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1226,7 +1227,6 @@
 		3BEA3ADE2D58F79700A67A1D /* OrefFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrefFunction.swift; sourceTree = "<group>"; };
 		3BEF6AB02D9731530076089D /* MealHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealHistoryTests.swift; sourceTree = "<group>"; };
 		3BEF6AB22D97316A0076089D /* MealTotalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealTotalTests.swift; sourceTree = "<group>"; };
-		3BEF6AB42D9750330076089D /* meal-input-sim.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "meal-input-sim.json"; sourceTree = "<group>"; };
 		3BEF6AB62D9750710076089D /* MealJsonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealJsonTests.swift; sourceTree = "<group>"; };
 		3BF424C62DF480550017CFD9 /* AutosensError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensError.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
@@ -2804,7 +2804,6 @@
 			children = (
 				3B8B5D2C2DF5234C00365ED3 /* autosens */,
 				3BF92F392D86F1AA006B545A /* iob-error-log.json */,
-				3BEF6AB42D9750330076089D /* meal-input-sim.json */,
 			);
 			path = json;
 			sourceTree = "<group>";
@@ -2813,6 +2812,7 @@
 			isa = PBXGroup;
 			children = (
 				3B1C5C3D2D68E269004E9273 /* Extensions.swift */,
+				3B4821812E080CAE00F0DD17 /* HttpFiles.swift */,
 				3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */,
 				3BF92F372D86E106006B545A /* OpenAPSFixed.swift */,
 				3B8B5D3D2DF5240600365ED3 /* TimeZoneForTests.swift */,
@@ -4389,7 +4389,6 @@
 				3BF92F312D86DEE9006B545A /* meal.js in Resources */,
 				3BF92F322D86DEE9006B545A /* glucose-get-last.js in Resources */,
 				3BF92F332D86DEE9006B545A /* iob.js in Resources */,
-				3BEF6AB52D9750330076089D /* meal-input-sim.json in Resources */,
 				3BF92F352D86DEE9006B545A /* basal-set-temp.js in Resources */,
 				3BF92F362D86DEE9006B545A /* autotune-core.js in Resources */,
 				3BC0AA3B2DA74C87000DF7B7 /* iob-total.js in Resources */,
@@ -5178,6 +5177,7 @@
 				3B1C5C432D68E269004E9273 /* Extensions.swift in Sources */,
 				3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */,
 				3BF92F382D86E10B006B545A /* OpenAPSFixed.swift in Sources */,
+				3B4821822E080CAE00F0DD17 /* HttpFiles.swift in Sources */,
 				3B1C5C442D68E269004E9273 /* IobJsonTypes.swift in Sources */,
 				3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */,
 				3BE2F1EA2E031951009E2900 /* MealCobBucketingTests.swift in Sources */,

+ 74 - 18
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -332,7 +332,8 @@ final class OpenAPS {
             basalProfile: basalProfile,
             clock: clock,
             carbs: carbsAsJSON,
-            glucose: glucoseAsJSON
+            glucose: glucoseAsJSON,
+            useSwiftOref: useSwiftOref
         )
 
         // IOB calculation
@@ -659,25 +660,80 @@ final class OpenAPS {
         basalProfile: JSON,
         clock: JSON,
         carbs: JSON,
-        glucose: JSON
+        glucose: 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.meal),
-                    Script(name: Prepare.meal)
-                ])
-                let result = worker.call(function: Function.generate, with: [
-                    pumphistory,
-                    profile,
-                    clock,
-                    glucose,
-                    basalProfile,
-                    carbs
-                ])
-                continuation.resume(returning: result)
+        let startJavascriptAt = Date()
+        let jsResult = await mealJavascript(
+            pumphistory: pumphistory,
+            profile: profile,
+            basalProfile: basalProfile,
+            clock: clock,
+            carbs: carbs,
+            glucose: glucose
+        )
+        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, mealInputs) = OpenAPSSwift
+            .meal(
+                pumphistory: pumphistory,
+                profile: profile,
+                basalProfile: basalProfile,
+                clock: clock,
+                carbs: carbs,
+                glucose: glucose
+            )
+        let swiftDuration = Date().timeIntervalSince(startSwiftAt)
+
+        JSONCompare.logDifferences(
+            function: .meal,
+            swift: swiftResult,
+            swiftDuration: swiftDuration,
+            javascript: jsResult,
+            javascriptDuration: javascriptDuration,
+            mealInputs: mealInputs
+        )
+
+        return try jsResult.returnOrThrow()
+    }
+
+    private func mealJavascript(
+        pumphistory: JSON,
+        profile: JSON,
+        basalProfile: JSON,
+        clock: JSON,
+        carbs: JSON,
+        glucose: 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.meal),
+                        Script(name: Prepare.meal)
+                    ])
+                    let result = worker.call(function: Function.generate, with: [
+                        pumphistory,
+                        profile,
+                        clock,
+                        glucose,
+                        basalProfile,
+                        carbs
+                    ])
+                    continuation.resume(returning: result)
+                }
             }
+            return .success(result)
+        } catch {
+            return .failure(error)
         }
     }
 

+ 8 - 0
Trio/Sources/APS/OpenAPSSwift/JSONBridge.swift

@@ -44,6 +44,14 @@ enum JSONBridge {
         try JSONBridge.from(string: from.rawJSON)
     }
 
+    static func glucose(from: JSON) throws -> [BloodGlucose] {
+        try JSONBridge.from(string: from.rawJSON)
+    }
+
+    static func carbs(from: JSON) throws -> [CarbsEntry] {
+        try JSONBridge.from(string: from.rawJSON)
+    }
+
     static func pumpHistory(from: JSON) throws -> [PumpHistoryEvent] {
         do {
             return try JSONBridge.from(string: from.rawJSON)

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

@@ -71,6 +71,16 @@ struct IobInputs: Codable {
     let autosens: Autosens?
 }
 
+/// For tracking inputs to `meal` when there is a mismatch
+struct MealInputs: Codable {
+    let pumpHistory: [PumpHistoryEvent]
+    let profile: Profile
+    let basalProfile: [BasalProfileEntry]
+    let clock: Date
+    let carbs: [CarbsEntry]
+    let glucose: [BloodGlucose]
+}
+
 /// Represents a complete comparison between JS and Swift implementations
 struct AlgorithmComparison: Codable {
     let id: UUID
@@ -96,6 +106,7 @@ struct AlgorithmComparison: Codable {
 
     // Inputs for mismatches
     let iobInput: IobInputs?
+    let mealInput: MealInputs?
 
     init(
         function: OrefFunction,
@@ -107,6 +118,7 @@ struct AlgorithmComparison: Codable {
         swiftException: AlgorithmException? = nil,
         comparisonError: AlgorithmException? = nil,
         iobInputs: IobInputs? = nil,
+        mealInputs: MealInputs? = nil,
         id: UUID = UUID(),
         createdAt: Date = Date()
     ) {
@@ -121,8 +133,9 @@ struct AlgorithmComparison: Codable {
         self.swiftException = swiftException
         self.comparisonError = comparisonError
         iobInput = iobInputs
+        mealInput = mealInputs
         timezone = TimeZone.current.identifier
-        version = "2"
+        version = "3"
 
         #if targetEnvironment(simulator)
             isSimulator = true

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

@@ -84,7 +84,8 @@ enum JSONCompare {
         swiftDuration: TimeInterval,
         javascript: OrefFunctionResult,
         javascriptDuration: TimeInterval,
-        iobInputs: IobInputs? = nil
+        iobInputs: IobInputs? = nil,
+        mealInputs: MealInputs? = nil
     ) {
         let comparison = createComparison(
             function: function,
@@ -92,7 +93,8 @@ enum JSONCompare {
             swiftDuration: swiftDuration,
             javascript: javascript,
             javascriptDuration: javascriptDuration,
-            iobInputs: iobInputs
+            iobInputs: iobInputs,
+            mealInputs: mealInputs
         )
 
         Task {
@@ -110,7 +112,8 @@ enum JSONCompare {
         swiftDuration: TimeInterval,
         javascript: OrefFunctionResult,
         javascriptDuration: TimeInterval,
-        iobInputs: IobInputs?
+        iobInputs: IobInputs?,
+        mealInputs: MealInputs?
     ) -> AlgorithmComparison {
         switch (swift, javascript) {
         case let (.success(swiftJson), .success(javascriptJson)):
@@ -123,7 +126,8 @@ enum JSONCompare {
                     jsDuration: javascriptDuration,
                     swiftDuration: swiftDuration,
                     differences: differences.isEmpty ? nil : differences,
-                    iobInputs: differences.isEmpty ? nil : iobInputs
+                    iobInputs: differences.isEmpty ? nil : iobInputs,
+                    mealInputs: differences.isEmpty ? nil : mealInputs
                 )
             } catch {
                 return AlgorithmComparison(
@@ -149,7 +153,8 @@ enum JSONCompare {
                 resultType: .swiftOnlyException,
                 jsDuration: javascriptDuration,
                 swiftException: AlgorithmException(error: swiftError),
-                iobInputs: iobInputs
+                iobInputs: iobInputs,
+                mealInputs: mealInputs
             )
 
         case let (.success, .failure(jsError)):
@@ -158,7 +163,8 @@ enum JSONCompare {
                 resultType: .jsOnlyException,
                 swiftDuration: swiftDuration,
                 jsException: AlgorithmException(error: jsError),
-                iobInputs: iobInputs
+                iobInputs: iobInputs,
+                mealInputs: mealInputs
             )
         }
     }

+ 16 - 0
Trio/Sources/APS/OpenAPSSwift/Logging/OrefFunction.swift

@@ -23,6 +23,7 @@ enum OrefFunction: String, Codable {
 
     case makeProfile
     case iob
+    case meal
 
     // 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
@@ -34,6 +35,10 @@ enum OrefFunction: String, Codable {
         case .iob:
             // we're only checking the first result for now
             return Set(stride(from: 1, to: 48, by: 1).map { String("[\($0)]") })
+        case .meal:
+            // These aren't used by downstream calculations, so we
+            // can ignore them in our comparison
+            return Set(["maxDeviation", "minDeviation", "allDeviations", "bwCarbs", "bwFound", "journalCarbs", "nsCarbs"])
         }
     }
 
@@ -57,6 +62,15 @@ enum OrefFunction: String, Codable {
                 // https://github.com/nightscout/Trio-dev/issues/453
                 "duration": 120
             ]
+        case .meal:
+            return [
+                "carbs": 0.1,
+                "mealCOB": 10,
+                "currentDeviation": 1,
+                "slopeFromMaxDeviation": 0.25,
+                "slopeFromMinDeviation": 0.25,
+                "lastCarbTime": 1
+            ]
         }
     }
 
@@ -66,6 +80,8 @@ enum OrefFunction: String, Codable {
             return .dictionary
         case .iob:
             return .array
+        case .meal:
+            return .dictionary
         }
     }
 }

+ 7 - 5
Trio/Sources/APS/OpenAPSSwift/Meal/MealCob.swift

@@ -30,6 +30,7 @@ struct MealCob {
     ///
     /// This is the main COB detection algorithm entry point
     static func detectCarbAbsorption(
+        clock: Date,
         glucose: [BloodGlucose],
         pumpHistory: [PumpHistoryEvent],
         basalProfile: [BasalProfileEntry],
@@ -40,7 +41,7 @@ struct MealCob {
         let treatments = try IobHistory.calcTempTreatments(
             history: pumpHistory.map { $0.computedEvent() },
             profile: profile,
-            clock: mealDate,
+            clock: clock,
             autosens: nil,
             zeroTempDuration: nil
         )
@@ -179,7 +180,7 @@ struct MealCob {
             let iob = try IobCalculation.iobTotal(treatments: treatments, profile: simulationProfile, time: glucoseTime)
 
             // Copying Javascript rounding
-            // JS oref calls this "big" = "blood glucose impact"
+            // JS oref calls this "bgi" = "blood glucose impact"
             let glucoseImpact: Decimal = (-iob.activity * sensitivity * 5 * 100 + 0.5)
                 .rounded(scale: 0, roundingMode: .down) / 100
             let deviation = delta - glucoseImpact
@@ -192,8 +193,9 @@ struct MealCob {
                 }
             } else if let carbImpactDate = carbImpactDate, carbImpactDate > glucoseTime {
                 let avgDeviation = ((avgDelta - glucoseImpact) * 1000).rounded() / 1000
+                // we remove the * 1000 because we're already using seconds, not ms
                 let deviationSlope = (avgDeviation - currentDeviation) / Decimal(glucoseTime.timeIntervalSince(carbImpactDate)) *
-                    1000 * 60 * 5
+                    60 * 5
 
                 if avgDeviation > maxDeviation {
                     slopeFromMaxDeviation = min(0, deviationSlope)
@@ -214,8 +216,8 @@ struct MealCob {
                 }
 
                 // Figure out how many carbs that represents
-                let ci = max(deviation, currentDeviation / 2, profile.min5mCarbImpact)
-                let absorbed = ci * carbRatio / sensitivity
+                let carbImpact = max(deviation, currentDeviation / 2, profile.min5mCarbImpact)
+                let absorbed = carbImpact * carbRatio / sensitivity
                 carbsAbsorbed += absorbed
             }
         }

+ 1 - 3
Trio/Sources/APS/OpenAPSSwift/Meal/MealGenerator.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-enum MealGeneratorError {
+enum MealGenerator {
     static func generate(
         pumpHistory: [PumpHistoryEvent],
         profile: Profile,
@@ -11,8 +11,6 @@ enum MealGeneratorError {
     ) throws -> ComputedCarbs? {
         let treatments: [MealInput] = MealHistory.findMealInputs(pumpHistory: pumpHistory, carbHistory: carbHistory)
 
-        // TODO: do we need to handle the clock timezone handling? We'll parse in a proper Swift Date anyhow
-
         return try MealTotal.recentCarbs(
             treatments: treatments,
             pumpHistory: pumpHistory,

+ 5 - 2
Trio/Sources/APS/OpenAPSSwift/Meal/MealTotal.swift

@@ -15,6 +15,7 @@ struct ComputedCarbs: Codable {
 struct IOBInput {
     let profile: Profile
     let history: [PumpHistoryEvent]
+    let clock: Date
 }
 
 struct COBInputs {
@@ -78,7 +79,7 @@ enum MealTotal {
         let mealCarbTime: TimeInterval = time.timeIntervalSince1970
         var lastCarbTime: TimeInterval = 0
 
-        let iobInputs = IOBInput(profile: profile, history: pumpHistory)
+        let iobInputs = IOBInput(profile: profile, history: pumpHistory, clock: time)
         var cobInputs = COBInputs(
             glucoseData: glucose,
             iobInputs: iobInputs,
@@ -110,6 +111,7 @@ enum MealTotal {
                     lastCarbTime = max(lastCarbTime, treatmentTime)
 
                     let myCarbsAbsorbed = try MealCob.detectCarbAbsorption(
+                        clock: cobInputs.iobInputs.clock,
                         glucose: cobInputs.glucoseData,
                         pumpHistory: cobInputs.iobInputs.history,
                         basalProfile: cobInputs.basalProfile,
@@ -143,6 +145,7 @@ enum MealTotal {
         /// omiting maxCOB check here, the setting is not Optional in Swift and must be part of profile
 
         let finalCobResult = try MealCob.detectCarbAbsorption(
+            clock: cobInputs.iobInputs.clock,
             glucose: cobInputs.glucoseData,
             pumpHistory: cobInputs.iobInputs.history,
             basalProfile: cobInputs.basalProfile,
@@ -165,7 +168,7 @@ enum MealTotal {
             slopeFromMaxDeviation: finalCobResult.slopeFromMaxDeviation.rounded(scale: 3),
             slopeFromMinDeviation: finalCobResult.slopeFromMinDeviation.rounded(scale: 3),
             allDeviations: finalCobResult.allDeviations,
-            lastCarbTime: lastCarbTime
+            lastCarbTime: (lastCarbTime * 1000).rounded()
         )
     }
 }

+ 42 - 0
Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift

@@ -41,6 +41,48 @@ struct OpenAPSSwift {
         }
     }
 
+    static func meal(
+        pumphistory: JSON,
+        profile: JSON,
+        basalProfile: JSON,
+        clock: JSON,
+        carbs: JSON,
+        glucose: JSON
+    ) -> (OrefFunctionResult, MealInputs?) {
+        var mealInputs: MealInputs?
+
+        do {
+            let pumpHistory = try JSONBridge.pumpHistory(from: pumphistory)
+            let profile = try JSONBridge.profile(from: profile)
+            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
+            let clock = try JSONBridge.clock(from: clock)
+            let carbs = try JSONBridge.carbs(from: carbs)
+            let glucose = try JSONBridge.glucose(from: glucose)
+
+            mealInputs = MealInputs(
+                pumpHistory: pumpHistory,
+                profile: profile,
+                basalProfile: basalProfile,
+                clock: clock,
+                carbs: carbs,
+                glucose: glucose
+            )
+
+            let mealResult = try MealGenerator.generate(
+                pumpHistory: pumpHistory,
+                profile: profile,
+                basalProfile: basalProfile,
+                clock: clock,
+                carbHistory: carbs,
+                glucoseHistory: glucose
+            )
+
+            return try (.success(JSONBridge.to(mealResult)), mealInputs)
+        } catch {
+            return (.failure(error), mealInputs)
+        }
+    }
+
     static func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) -> (OrefFunctionResult, IobInputs?) {
         var iobInputs: IobInputs?
 

+ 7 - 11
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -40,18 +40,12 @@ import Testing
     // Note: This test case has a memory leak so limit your inputs
     // to about 250 files at a time
     @Test(
-        "should produce same results for fixed JS and different for bundle JS",
+        "IoB should produce same results for fixed JS and different for bundle JS",
         .enabled(if: false)
     ) func replayErrorInputs() async throws {
-        let url = URL(string: "http://localhost:8123/list")!
-        let (data, _) = try await URLSession.shared.data(from: url)
-        let files = try JSONDecoder().decode([String].self, from: data)
-        let fileDataDecoder = JSONDecoder()
-        fileDataDecoder.dateDecodingStrategy = .secondsSince1970
+        let files = try await HttpFiles.listFiles()
         for filePath in files {
-            let dataUrl = URL(string: "http://localhost:8123\(filePath)")!
-            let (data, _) = try await URLSession.shared.data(from: dataUrl)
-            let algorithmComparison = try fileDataDecoder.decode(AlgorithmComparison.self, from: data)
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
             print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
             guard let iobInputs = algorithmComparison.iobInput else {
                 print("Skipping, no iobInputs found")
@@ -95,7 +89,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: iobResultJavascript,
             javascriptDuration: 0.1,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         if comparison.resultType == .valueDifference {
@@ -131,7 +126,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: iobResultJavascript,
             javascriptDuration: 0.1,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         if comparison.resultType != .valueDifference {

+ 7 - 0
TrioTests/OpenAPSSwiftTests/MealCobTests.swift

@@ -50,6 +50,7 @@ import Testing
 
         // Test with carbImpactTime
         var result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -62,6 +63,7 @@ import Testing
 
         // Test without carbImpactTime
         result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -86,6 +88,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -110,6 +113,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -150,6 +154,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -183,6 +188,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -213,6 +219,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: mealTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,

+ 143 - 39
TrioTests/OpenAPSSwiftTests/MealJsonTests.swift

@@ -2,46 +2,150 @@ import Foundation
 import Testing
 @testable import Trio
 
-@Suite("Meal testing using JSON inputs") struct MealJsonTests {
-    @Test("Test against simulator inputs") func simulatorInputs() throws {
-        let testBundle = Bundle(for: BundleReference.self)
-        let path = testBundle.path(forResource: "meal-input-sim", ofType: "json")!
-        let data = try Data(contentsOf: URL(fileURLWithPath: path))
-
-        // this file stores an object with JSON encoded strings (so double encoded)
-        let jsonInputs = try JSONSerialization.jsonObject(with: data) as! [String: Any]
-
-        let pumpHistory = try JSONBridge.pumpHistory(from: jsonInputs["pumpHistory"] as! String)
-        let profile = try JSONBridge.profile(from: jsonInputs["profile"] as! String)
-        let basalProfile = try JSONBridge.basalProfile(from: jsonInputs["basalProfile"] as! String)
-        let clock = try JSONBridge.clock(from: jsonInputs["clock"] as! String)
-
-        let decoder = JSONCoding.decoder
-        var jsonData = (jsonInputs["carbs"] as! String).data(using: .utf8)!
-        let carbHistory: [CarbsEntry] = try decoder.decode([CarbsEntry].self, from: jsonData)
-
-        jsonData = (jsonInputs["glucose"] as! String).data(using: .utf8)!
-        let glucoseHistory: [BloodGlucose] = try decoder.decode([BloodGlucose].self, from: jsonData)
-
-        jsonData = (jsonInputs["meal"] as! String).data(using: .utf8)!
-        let mealResultFromJs = try decoder.decode(ComputedCarbs.self, from: jsonData)
-
-        let mealResult = try MealGeneratorError.generate(
-            pumpHistory: pumpHistory,
-            profile: profile,
-            basalProfile: basalProfile,
-            clock: clock,
-            carbHistory: carbHistory,
-            glucoseHistory: glucoseHistory
+@Suite("Meal testing using JSON inputs", .serialized) struct MealJsonTests {
+    let timeZoneForTests = TimeZoneForTests()
+
+    @Test(
+        "Meal should produce same results for fixed JS",
+        .enabled(if: false)
+    ) func replayErrorInputs() async throws {
+        // Note: This test case can only test one timezone per invocation
+        // so you need to manually change this to try out errors from
+        // different timezones
+        let testingTimezone = "Europe/Berlin"
+        let files = try await HttpFiles.listFiles()
+        var skippedTimezones = Set<String>()
+        for filePath in files {
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
+            print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
+            guard algorithmComparison.timezone == testingTimezone else {
+                print("Skipping timezone \(algorithmComparison.timezone)")
+                skippedTimezones.insert(algorithmComparison.timezone)
+                continue
+            }
+            guard let mealInputs = algorithmComparison.mealInput else {
+                print("Skipping, no mealInputs found")
+                if let str = algorithmComparison.comparisonError {
+                    print(str)
+                }
+                if let str = algorithmComparison.swiftException {
+                    print(str)
+                }
+                continue
+            }
+
+            timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+            try await checkFixedJsAgainstSwift(mealInputs: mealInputs)
+            print("Checked \(filePath) \(algorithmComparison.timezone)")
+            timeZoneForTests.resetTimezone()
+        }
+
+        if skippedTimezones.isEmpty {
+            print("Didn't skip any timezones")
+        } else {
+            print("Skipped timezones:")
+            for timezone in skippedTimezones {
+                print("  - \(timezone)")
+            }
+        }
+    }
+
+    func checkFixedJsAgainstSwift(mealInputs: MealInputs) async throws {
+        let openAps = OpenAPSFixed()
+        let (mealResultSwift, _) = OpenAPSSwift.meal(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
         )
 
-        #expect(mealResult?.mealCOB == mealResultFromJs.mealCOB)
-        #expect(mealResult?.carbs == mealResultFromJs.carbs)
-        #expect(mealResult?.currentDeviation == mealResultFromJs.currentDeviation)
-        // https://github.com/nightscout/Trio-dev/issues/539
-        // Ignore this check due to Issue 539
-        // #expect(mealResult?.allDeviations == mealResultFromJs.allDeviations)
-        #expect(mealResult?.maxDeviation == mealResultFromJs.maxDeviation)
-        #expect(mealResult?.minDeviation == mealResultFromJs.minDeviation)
+        let mealResultJavascript = await openAps.mealJavascript(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        let comparison = JSONCompare.createComparison(
+            function: .meal,
+            swift: mealResultSwift,
+            swiftDuration: 0.1,
+            javascript: mealResultJavascript,
+            javascriptDuration: 0.1,
+            iobInputs: nil,
+            mealInputs: 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)
+    }
+
+    @Test("Format 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
+        // 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/7a8a377e-f483-46a5-adbb-290baa04801b.3.json")
+        let mealInputs = algorithmComparison.mealInput!
+
+        let encoder = JSONCoding.encoder
+        let output = try encoder.encode(mealInputs)
+
+        let sharedDir = FileManager.default.temporaryDirectory
+        let outputURL = sharedDir.appendingPathComponent("meal_error_inputs.json")
+        // Print the path so you can find it
+        print("Writing to: \(outputURL.path)")
+        try output.write(to: outputURL)
+
+        timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+        let (mealResultSwift, _) = OpenAPSSwift.meal(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        print("Swift result")
+        switch mealResultSwift {
+        case let .success(rawJson):
+            print(rawJson)
+        case let .failure(error):
+            print(error.localizedDescription)
+        }
+
+        let mealResultJavascript = await openAps.mealJavascript(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        print("Fixed JS result")
+        switch mealResultJavascript {
+        case let .success(rawJson):
+            print(rawJson)
+        case let .failure(error):
+            print(error.localizedDescription)
+        }
+
+        timeZoneForTests.resetTimezone()
     }
 }

+ 1 - 1
TrioTests/OpenAPSSwiftTests/MealTotalTests.swift

@@ -100,7 +100,7 @@ import Testing
         #expect(result != nil)
         #expect(result!.mealCOB.isWithin(12 * 0.25, of: 12) == true, "mealCOB: \(result!.mealCOB.description)")
         #expect(
-            result!.currentDeviation.isWithin(3 * 0.25, of: 0) == true,
+            result!.currentDeviation.isWithin(3 * 0.25, of: 3),
             "currentDeviation: \(result!.currentDeviation.description)"
         )
     }

+ 2 - 1
TrioTests/OpenAPSSwiftTests/ProfileJavascriptTests.swift

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

+ 14 - 7
TrioTests/OpenAPSSwiftTests/ProfileJsNativeCompareTests.swift

@@ -95,7 +95,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: profileJs,
             javascriptDuration: 0.1,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -125,7 +126,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -144,7 +146,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .valueDifference)
@@ -165,7 +168,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: .failure(error),
             javascriptDuration: 0.2,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .matchingExceptions)
@@ -183,7 +187,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .swiftOnlyException)
@@ -203,7 +208,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: .failure(error),
             javascriptDuration: 0.2,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .jsOnlyException)
@@ -222,7 +228,8 @@ import Testing
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
             javascriptDuration: 0.2,
-            iobInputs: nil
+            iobInputs: nil,
+            mealInputs: nil
         )
 
         #expect(comparison.resultType == .comparisonError)

File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-core.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-prep.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/basal-set-temp.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/determine-basal.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/glucose-get-last.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-calculate.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-history.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-total.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/meal.js


File diff suppressed because it is too large
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/profile.js


File diff suppressed because it is too large
+ 0 - 16
TrioTests/OpenAPSSwiftTests/json/meal-input-sim.json


+ 23 - 0
TrioTests/OpenAPSSwiftTests/utils/HttpFiles.swift

@@ -0,0 +1,23 @@
+import Foundation
+@testable import Trio
+
+/// Helper struct to download files from localhost via HTTP. Must have a HTTP server
+/// running on port 8123 that supports listing files and downloading files
+///
+/// This struct is only useful during testing as it is missing a number of error checks
+struct HttpFiles {
+    static func listFiles() async throws -> [String] {
+        let url = URL(string: "http://localhost:8123/list")!
+        let (data, _) = try await URLSession.shared.data(from: url)
+        let files = try JSONDecoder().decode([String].self, from: data)
+        return files
+    }
+
+    static func downloadFile(at: String) async throws -> AlgorithmComparison {
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .secondsSince1970
+        let dataUrl = URL(string: "http://localhost:8123\(at)")!
+        let (data, _) = try await URLSession.shared.data(from: dataUrl)
+        return try decoder.decode(AlgorithmComparison.self, from: data)
+    }
+}

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

@@ -39,6 +39,40 @@ final class OpenAPSFixed {
         return result
     }
 
+    func mealJavascript(
+        pumphistory: JSON,
+        profile: JSON,
+        basalProfile: JSON,
+        clock: JSON,
+        carbs: JSON,
+        glucose: JSON
+    ) async -> OrefFunctionResult {
+        let testBundle = Bundle(for: OpenAPSFixed.self)
+        do {
+            let result = try await withCheckedThrowingContinuation { continuation in
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: "prepare/log.js"),
+                        Script.fromTestingBundle(name: "meal.js", bundle: testBundle),
+                        Script(name: "prepare/meal.js")
+                    ])
+                    let result = worker.call(function: "generate", with: [
+                        pumphistory,
+                        profile,
+                        clock,
+                        glucose,
+                        basalProfile,
+                        carbs
+                    ])
+                    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)