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

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

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

+ 172 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,7 +1,25 @@
 {
-  "originHash" : "e86ddc0787a4009654195f670305dba355987194a6030935dd73ee0db8da3359",
+  "originHash" : "89074a88ed67a58ecd7534519854c5a0928a4046d7c8a6123a7d70f27bf8b44d",
   "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",
@@ -11,6 +29,105 @@
       }
     },
     {
+      "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",
@@ -20,6 +137,33 @@
       }
     },
     {
+      "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",
@@ -29,6 +173,33 @@
       }
     },
     {
+      "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",

+ 108 - 31
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -371,7 +371,8 @@ final class OpenAPS {
             pumpHistory: pumpHistoryJSON,
             preferences: preferences,
             basalProfile: basalProfile,
-            trioCustomOrefVariables: trioCustomOrefVariables
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            useSwiftOref: useSwiftOref
         )
 
         debug(.openAPS, "\(simulation ? "[SIMULATION]" : "") OREF DETERMINATION: \(orefDetermination)")
@@ -834,40 +835,116 @@ final class OpenAPS {
         pumpHistory: JSON,
         preferences: JSON,
         basalProfile: JSON,
-        trioCustomOrefVariables: JSON
+        trioCustomOrefVariables: JSON,
+        useSwiftOref: Bool
     ) async throws -> RawJSON {
-        try await withCheckedThrowingContinuation { continuation in
-            jsWorker.inCommonContext { worker in
-                worker.evaluateBatch(scripts: [
-                    Script(name: Prepare.log),
-                    Script(name: Prepare.determineBasal),
-                    Script(name: Bundle.basalSetTemp),
-                    Script(name: Bundle.getLastGlucose),
-                    Script(name: Bundle.determineBasal)
-                ])
-
-                if let middleware = self.middlewareScript(name: OpenAPS.Middleware.determineBasal) {
-                    worker.evaluate(script: middleware)
-                }
+        let clock = Date()
+        let startJavascriptAt = Date()
+        let jsResult = await determineBasalJavascript(
+            glucose: glucose,
+            currentTemp: currentTemp,
+            iob: iob,
+            profile: profile,
+            autosens: autosens,
+            meal: meal,
+            microBolusAllowed: microBolusAllowed,
+            reservoir: reservoir,
+            pumpHistory: pumpHistory,
+            preferences: preferences,
+            basalProfile: basalProfile,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            clock: clock
+        )
+        let javascriptDuration = Date().timeIntervalSince(startJavascriptAt)
 
-                let result = worker.call(function: Function.generate, with: [
-                    iob,
-                    currentTemp,
-                    glucose,
-                    profile,
-                    autosens,
-                    meal,
-                    microBolusAllowed,
-                    reservoir,
-                    Date(),
-                    pumpHistory,
-                    preferences,
-                    basalProfile,
-                    trioCustomOrefVariables
-                ])
+        // Important: we want to make sure that this flag ensures that none
+        // of the native code runs
+        guard useSwiftOref else {
+            return try jsResult.returnOrThrow()
+        }
 
-                continuation.resume(returning: result)
+        let startSwiftAt = Date()
+        let (swiftResult, determineBasalInputs) = OpenAPSSwift.determineBasal(
+            glucose: glucose,
+            currentTemp: currentTemp,
+            iob: iob,
+            profile: profile,
+            autosens: autosens,
+            meal: meal,
+            microBolusAllowed: microBolusAllowed,
+            reservoir: reservoir,
+            pumpHistory: pumpHistory,
+            preferences: preferences,
+            basalProfile: basalProfile,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            clock: clock
+        )
+        let swiftDuration = Date().timeIntervalSince(startSwiftAt)
+
+        JSONCompare.logDifferences(
+            function: .determineBasal,
+            swift: swiftResult,
+            swiftDuration: swiftDuration,
+            javascript: jsResult,
+            javascriptDuration: javascriptDuration,
+            determineBasalInputs: determineBasalInputs
+        )
+
+        return try jsResult.returnOrThrow()
+    }
+
+    private func determineBasalJavascript(
+        glucose: JSON,
+        currentTemp: JSON,
+        iob: JSON,
+        profile: JSON,
+        autosens: JSON,
+        meal: JSON,
+        microBolusAllowed: Bool,
+        reservoir: JSON,
+        pumpHistory: JSON,
+        preferences: JSON,
+        basalProfile: JSON,
+        trioCustomOrefVariables: JSON,
+        clock: Date
+    ) async -> OrefFunctionResult {
+        do {
+            let result = try await withCheckedThrowingContinuation { continuation in
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: Prepare.log),
+                        Script(name: Prepare.determineBasal),
+                        Script(name: Bundle.basalSetTemp),
+                        Script(name: Bundle.getLastGlucose),
+                        Script(name: Bundle.determineBasal)
+                    ])
+
+                    if let middleware = self.middlewareScript(name: OpenAPS.Middleware.determineBasal) {
+                        worker.evaluate(script: middleware)
+                    }
+
+                    let result = worker.call(function: Function.generate, with: [
+                        iob,
+                        currentTemp,
+                        glucose,
+                        profile,
+                        autosens,
+                        meal,
+                        microBolusAllowed,
+                        reservoir,
+                        clock,
+                        pumpHistory,
+                        preferences,
+                        basalProfile,
+                        trioCustomOrefVariables
+                    ])
+
+                    continuation.resume(returning: result)
+                }
             }
+            return .success(result)
+        } catch {
+            return .failure(error)
         }
     }
 

+ 89 - 0
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasal+Helpers.swift

@@ -1,6 +1,95 @@
 import Foundation
 
 extension DeterminationGenerator {
+    /// Smooths given CGM readings, and computes rolling delta statistics
+    /// (i.e., last, short-term, and long-term).
+    ///
+    /// Mirrors JavaScript oref `glucose-get-last.js` logic.
+    ///
+    /// - Returns: A `GlucoseStatus` containing:
+    ///   - `glucose`: the most recent glucose value (mg/dL),
+    ///   - `delta`: the 5-minute delta (mg/dL per 5m),
+    ///   - `shortAvgDelta`: the average delta over ~5–15 minutes,
+    ///   - `longAvgDelta`: the average delta over ~20–40 minutes,
+    ///   - `noise`: the CGM noise level (if any),
+    ///   - `date`: the timestamp of the “now” reading,
+    ///   - `lastCalIndex`: index of the last calibration record (always `nil` here),
+    ///   - `device`: the source device string.
+    ///
+    /// - Throws: Any `CoreDataError` or other error encountered during fetch or context work.
+    /// - Returns: `nil` if no valid glucose readings are found in the past day.
+    static func getGlucoseStatus(glucoseReadings: [BloodGlucose]) throws -> GlucoseStatus? {
+        // FIXME: put this here for now; use implementation in GlucoseStorage later (already implemented and commented out for now)
+        guard glucoseReadings.isNotEmpty else {
+            return nil
+        }
+
+        // Sort descending (newest first)
+        let sorted = glucoseReadings.sorted { $0.date > $1.date }
+
+        let mostRecentGlucose = sorted[0]
+        var mostRecentGlucoseReading: Int = mostRecentGlucose.glucose!
+        var mostRecentGlucoseDate: Date = mostRecentGlucose.dateString
+
+        var lastDeltas: [Decimal] = []
+        var shortDeltas: [Decimal] = []
+        var longDeltas: [Decimal] = []
+
+        // Walk older entries to compute deltas
+        for entry in sorted.dropFirst() {
+            // JS oref has logic here around skipping calibration readings.
+            // We never calibration record (never happens here, since type=="sgv")
+            // so we omit this check
+
+            // only use readings >38 mg/dL (to skip code values, <39)
+            guard let glucose = entry.glucose, glucose > 38 else { continue }
+
+            let minutesAgo = mostRecentGlucoseDate.timeIntervalSince(entry.dateString) / 60
+            guard minutesAgo != 0 else { continue }
+            // compute mg/dL per 5 m as a Decimal:
+            let change = Decimal(mostRecentGlucoseReading - glucose)
+            let avgDelta = (change / Decimal(minutesAgo)) * Decimal(5)
+
+            // very-recent (<2.5 m) smooths "now"
+            if minutesAgo > -2, minutesAgo <= 2.5 {
+                mostRecentGlucoseReading = (mostRecentGlucoseReading + glucose) / 2
+                mostRecentGlucoseDate = Date(
+                    timeIntervalSince1970: (
+                        mostRecentGlucoseDate.timeIntervalSince1970 + entry.dateString
+                            .timeIntervalSince1970
+                    ) / 2
+                )
+            }
+            // short window (~5–15 m)
+            else if minutesAgo > 2.5, minutesAgo <= 17.5 {
+                shortDeltas.append(avgDelta)
+                if minutesAgo < 7.5 {
+                    lastDeltas.append(avgDelta)
+                }
+            }
+            // long window (~20–40 m)
+            else if minutesAgo > 17.5, minutesAgo < 42.5 {
+                longDeltas.append(avgDelta)
+            }
+        }
+
+        // compute means (or zero)
+        let lastDelta: Decimal = lastDeltas.mean
+        let shortAvg: Decimal = shortDeltas.mean
+        let longAvg: Decimal = longDeltas.mean
+
+        return GlucoseStatus(
+            delta: lastDelta.rounded(toPlaces: 2),
+            glucose: Decimal(mostRecentGlucoseReading),
+            noise: Int(sorted[0].noise ?? 0),
+            shortAvgDelta: shortAvg.rounded(toPlaces: 2),
+            longAvgDelta: longAvg.rounded(toPlaces: 2),
+            date: mostRecentGlucoseDate,
+            lastCalIndex: nil,
+            device: "", // FIXME: will be filled once this gets moved back to GlucoseStorage
+        )
+    }
+
     static func calculateExpectedDelta(
         targetGlucose: Decimal,
         eventualGlucose: Decimal,

+ 4 - 2
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -17,10 +17,12 @@ enum DeterminationGenerator {
         iobData: [IobResult],
         mealData: ComputedCarbs,
         autosensData: Autosens,
-        reservoirData _: Reservoir,
-        glucoseStatus: GlucoseStatus?,
+        reservoirData _: Decimal,
+        glucose: [BloodGlucose],
         currentTime: Date
     ) throws -> Determination? {
+        let glucoseStatus = try Self.getGlucoseStatus(glucoseReadings: glucose)
+
         try checkDeterminationInputs(
             glucoseStatus: glucoseStatus,
             currentTemp: currentTemp,

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

@@ -24,6 +24,10 @@ enum JSONBridge {
         try JSONBridge.from(string: from.rawJSON)
     }
 
+    static func trioCustomOrefVariables(from: JSON) throws -> TrioCustomOrefVariables {
+        try JSONBridge.from(string: from.rawJSON)
+    }
+
     static func insulinSensitivities(from: JSON) throws -> InsulinSensitivities {
         try JSONBridge.from(string: from.rawJSON)
     }
@@ -48,10 +52,18 @@ enum JSONBridge {
         try JSONBridge.from(string: from.rawJSON)
     }
 
+    static func currentTemp(from: JSON) throws -> TempBasal {
+        try JSONBridge.from(string: from.rawJSON)
+    }
+
     static func carbs(from: JSON) throws -> [CarbsEntry] {
         try JSONBridge.from(string: from.rawJSON)
     }
 
+    static func iobResult(from: JSON) throws -> [IobResult] {
+        try JSONBridge.from(string: from.rawJSON)
+    }
+
     static func pumpHistory(from: JSON) throws -> [PumpHistoryEvent] {
         do {
             return try JSONBridge.from(string: from.rawJSON)
@@ -75,6 +87,10 @@ enum JSONBridge {
         try JSONBridge.from(string: from.rawJSON)
     }
 
+    static func computedCarbs(from: JSON) throws -> ComputedCarbs? {
+        try JSONBridge.from(string: from.rawJSON)
+    }
+
     static func autosens(from: JSON) throws -> Autosens? {
         try JSONBridge.from(string: from.rawJSON)
     }

+ 21 - 2
Trio/Sources/APS/OpenAPSSwift/Logging/AlgorithmComparison.swift

@@ -92,6 +92,23 @@ struct AutosensInputs: Codable {
     let clock: Date
 }
 
+/// For tracking inputs to `determineBasal` when there is a mismatch
+struct DetermineBasalInputs: Codable {
+    let glucose: [BloodGlucose]
+    let currentTemp: TempBasal
+    let iob: [IobResult]
+    let profile: Profile
+    let autosens: Autosens?
+    let meal: ComputedCarbs?
+    let microBolusAllowed: Bool
+    let reservoir: Decimal?
+    let pumpHistory: [PumpHistoryEvent]
+    let preferences: Preferences
+    let basalProfile: [BasalProfileEntry]
+    let trioCustomOrefVariables: TrioCustomOrefVariables
+    let clock: Date
+}
+
 /// Represents a complete comparison between JS and Swift implementations
 struct AlgorithmComparison: Codable {
     let id: UUID
@@ -119,6 +136,7 @@ struct AlgorithmComparison: Codable {
     let iobInput: IobInputs?
     let mealInput: MealInputs?
     let autosensInput: AutosensInputs?
+    let determineBasalInput: DetermineBasalInputs?
 
     init(
         function: OrefFunction,
@@ -132,6 +150,7 @@ struct AlgorithmComparison: Codable {
         iobInputs: IobInputs? = nil,
         mealInputs: MealInputs? = nil,
         autosensInputs: AutosensInputs? = nil,
+        determineBasalInputs: DetermineBasalInputs? = nil,
         id: UUID = UUID(),
         createdAt: Date = Date()
     ) {
@@ -148,9 +167,9 @@ struct AlgorithmComparison: Codable {
         iobInput = iobInputs
         mealInput = mealInputs
         autosensInput = autosensInputs
+        determineBasalInput = determineBasalInputs
         timezone = TimeZone.current.identifier
-        version = "3"
-
+        version = "4"
         #if targetEnvironment(simulator)
             isSimulator = true
         #else

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

@@ -86,7 +86,8 @@ enum JSONCompare {
         javascriptDuration: TimeInterval,
         iobInputs: IobInputs? = nil,
         mealInputs: MealInputs? = nil,
-        autosensInputs: AutosensInputs? = nil
+        autosensInputs: AutosensInputs? = nil,
+        determineBasalInputs: DetermineBasalInputs? = nil
     ) {
         let comparison = createComparison(
             function: function,
@@ -96,7 +97,8 @@ enum JSONCompare {
             javascriptDuration: javascriptDuration,
             iobInputs: iobInputs,
             mealInputs: mealInputs,
-            autosensInputs: autosensInputs
+            autosensInputs: autosensInputs,
+            determineBasalInputs: determineBasalInputs
         )
 
         Task {
@@ -116,7 +118,8 @@ enum JSONCompare {
         javascriptDuration: TimeInterval,
         iobInputs: IobInputs?,
         mealInputs: MealInputs?,
-        autosensInputs: AutosensInputs?
+        autosensInputs: AutosensInputs?,
+        determineBasalInputs: DetermineBasalInputs?
     ) -> AlgorithmComparison {
         switch (swift, javascript) {
         case let (.success(swiftJson), .success(javascriptJson)):
@@ -131,7 +134,8 @@ enum JSONCompare {
                     differences: differences.isEmpty ? nil : differences,
                     iobInputs: differences.isEmpty ? nil : iobInputs,
                     mealInputs: differences.isEmpty ? nil : mealInputs,
-                    autosensInputs: differences.isEmpty ? nil : autosensInputs
+                    autosensInputs: differences.isEmpty ? nil : autosensInputs,
+                    determineBasalInputs: differences.isEmpty ? nil : determineBasalInputs
                 )
             } catch {
                 return AlgorithmComparison(
@@ -159,7 +163,8 @@ enum JSONCompare {
                 swiftException: AlgorithmException(error: swiftError),
                 iobInputs: iobInputs,
                 mealInputs: mealInputs,
-                autosensInputs: autosensInputs
+                autosensInputs: autosensInputs,
+                determineBasalInputs: determineBasalInputs
             )
 
         case let (.success, .failure(jsError)):
@@ -170,7 +175,8 @@ enum JSONCompare {
                 jsException: AlgorithmException(error: jsError),
                 iobInputs: iobInputs,
                 mealInputs: mealInputs,
-                autosensInputs: autosensInputs
+                autosensInputs: autosensInputs,
+                determineBasalInputs: determineBasalInputs
             )
         }
     }

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

@@ -25,6 +25,7 @@ enum OrefFunction: String, Codable {
     case iob
     case meal
     case makeProfile
+    case determineBasal
 
     // 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
@@ -42,6 +43,28 @@ enum OrefFunction: String, Codable {
             return Set(["maxDeviation", "minDeviation", "allDeviations", "bwCarbs", "bwFound", "journalCarbs", "nsCarbs"])
         case .autosens:
             return Set()
+        case .determineBasal:
+            // FIXME: Fill in the properties we don't check here
+            return Set([
+                "id",
+                "reason",
+                "units",
+                "insulinReq",
+                "rate",
+                "duration",
+                "deliverAt",
+                "carbsReq",
+                "temp",
+                "reservoir",
+                "ISF",
+                "current_target",
+                "TDD",
+                "insulinForManualBolus",
+                "manualBolusErrorString",
+                "minDelta",
+                "CR",
+                "received"
+            ])
         }
     }
 
@@ -80,6 +103,8 @@ enum OrefFunction: String, Codable {
                 "newisf": 1,
                 "deviationsUnsorted": 0.02
             ]
+        case .determineBasal:
+            return [:]
         }
     }
 
@@ -93,6 +118,8 @@ enum OrefFunction: String, Codable {
             return .dictionary
         case .autosens:
             return .dictionary
+        case .determineBasal:
+            return .dictionary
         }
     }
 }

+ 1 - 1
Trio/Sources/APS/OpenAPSSwift/Models/GlucoseStatus.swift

@@ -3,7 +3,7 @@ import Foundation
 /// Represents the computed status of the most recent CGM reading,
 /// including delta‐rates over various time windows for our
 /// swift-based Oref`DeterminationGenerator`.
-public struct GlucoseStatus {
+public struct GlucoseStatus: Codable {
     /// Immediate delta (mg/dL per 5 m) over the last ~5 m
     public let delta: Decimal
     /// The (“smoothed”) current glucose value (mg/dL)

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

@@ -41,6 +41,79 @@ struct OpenAPSSwift {
         }
     }
 
+    static func determineBasal(
+        glucose: JSON,
+        currentTemp: JSON,
+        iob: JSON,
+        profile: JSON,
+        autosens: JSON,
+        meal: JSON,
+        microBolusAllowed: Bool,
+        reservoir: JSON,
+        pumpHistory: JSON,
+        preferences: JSON,
+        basalProfile: JSON,
+        trioCustomOrefVariables: JSON,
+        clock: Date
+    ) -> (OrefFunctionResult, DetermineBasalInputs?) {
+        var determineBasalInputs: DetermineBasalInputs?
+
+        print(reservoir)
+
+        do {
+            let glucose = try JSONBridge.glucose(from: glucose)
+            let currentTemp = try JSONBridge.currentTemp(from: currentTemp)
+            let iob = try JSONBridge.iobResult(from: iob)
+            let profile = try JSONBridge.profile(from: profile)
+            let autosens = try JSONBridge.autosens(from: autosens)
+            let meal = try JSONBridge.computedCarbs(from: meal)
+            let microBolusAllowed = microBolusAllowed
+            let reservoir = Decimal(string: reservoir.rawJSON)
+            let pumpHistory = try JSONBridge.pumpHistory(from: pumpHistory)
+            let preferences = try JSONBridge.preferences(from: preferences)
+            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
+            let trioCustomOrefVariables = try JSONBridge.trioCustomOrefVariables(from: trioCustomOrefVariables)
+
+            determineBasalInputs = DetermineBasalInputs(
+                glucose: glucose,
+                currentTemp: currentTemp,
+                iob: iob,
+                profile: profile,
+                autosens: autosens,
+                meal: meal,
+                microBolusAllowed: microBolusAllowed,
+                reservoir: reservoir,
+                pumpHistory: pumpHistory,
+                preferences: preferences,
+                basalProfile: basalProfile,
+                trioCustomOrefVariables: trioCustomOrefVariables,
+                clock: clock
+            )
+
+            guard let mealData = meal, let autosensData = autosens else {
+                return (.failure(DeterminationError.missingInputs), determineBasalInputs)
+            }
+
+            let rawDetermination = try DeterminationGenerator.generate(
+                profile: profile,
+                currentTemp: currentTemp,
+                iobData: iob,
+                mealData: mealData,
+                autosensData: autosensData,
+                reservoirData: reservoir ?? 100,
+                glucose: glucose,
+                currentTime: clock
+            )
+
+            // FIXME: fill in with result once we have it
+            return (try .success(JSONBridge.to(rawDetermination)), determineBasalInputs)
+//            return (.success(RawJSON.null), determineBasalInputs)
+
+        } catch {
+            return (.failure(error), determineBasalInputs)
+        }
+    }
+
     static func meal(
         pumphistory: JSON,
         profile: JSON,

+ 122 - 121
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -23,7 +23,7 @@ protocol GlucoseStorage {
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
     func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
-    func getGlucoseStatus() async throws -> GlucoseStatus?
+//    func getGlucoseStatus() async throws -> GlucoseStatus? // FIXME: prepared for later use
     var alarm: GlucoseAlarm? { get }
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
@@ -545,126 +545,127 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         }
     }
 
-    /// Fetches the most recent glucose readings from Core Data, filters and smooths them,
-    /// and computes rolling delta statistics (last, short-term, and long-term).
-    ///
-    /// Mirrors JavaScript oref `glucose-get-last.js` logic.
-    ///
-    /// - Returns: A `GlucoseStatus` containing:
-    ///   - `glucose`: the most recent glucose value (mg/dL),
-    ///   - `delta`: the 5-minute delta (mg/dL per 5m),
-    ///   - `shortAvgDelta`: the average delta over ~5–15 minutes,
-    ///   - `longAvgDelta`: the average delta over ~20–40 minutes,
-    ///   - `noise`: the CGM noise level (if any),
-    ///   - `date`: the timestamp of the “now” reading,
-    ///   - `lastCalIndex`: index of the last calibration record (always `nil` here),
-    ///   - `device`: the source device string.
-    ///
-    /// - Throws: Any `CoreDataError` or other error encountered during fetch or context work.
-    /// - Returns: `nil` if no valid glucose readings are found in the past day.
-    public func getGlucoseStatus() async throws -> GlucoseStatus? {
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate(
-                format: "date >= %@ AND isManual == %@",
-                Date.oneDayAgoInMinutes as NSDate,
-                false as NSNumber
-            ),
-            key: "date",
-            ascending: false
-        )
-
-        guard let stored = results as? [GlucoseStored], !stored.isEmpty else {
-            return nil
-        }
-
-        let validReadings: [BloodGlucose] = await context.perform {
-            stored.compactMap { entry in
-                BloodGlucose(
-                    _id: entry.id?.uuidString ?? UUID().uuidString,
-                    sgv: Int(entry.glucose),
-                    direction: BloodGlucose.Direction(from: entry.direction ?? ""),
-                    date: Decimal(entry.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
-                    dateString: entry.date ?? Date(),
-                    unfiltered: Decimal(entry.glucose),
-                    filtered: Decimal(entry.glucose),
-                    noise: nil,
-                    glucose: Int(entry.glucose),
-                    type: "sgv"
-                )
-            }
-        }
-
-        guard !validReadings.isEmpty else {
-            return nil
-        }
-
-        // Sort descending (newest first)
-        let sorted = validReadings.sorted { $0.date > $1.date }
-
-        let mostRecentGlucose = sorted[0]
-        var mostRecentGlucoseReading: Int = mostRecentGlucose.glucose!
-        var mostRecentGlucoseDate: Date = mostRecentGlucose.dateString
-
-        var lastDeltas: [Decimal] = []
-        var shortDeltas: [Decimal] = []
-        var longDeltas: [Decimal] = []
-
-        // Walk older entries to compute deltas
-        for entry in sorted.dropFirst() {
-            // JS oref has logic here around skipping calibration readings.
-            // We never calibration record (never happens here, since type=="sgv")
-            // so we omit this check
-
-            // only use readings >38 mg/dL (to skip code values, <39)
-            guard let glucose = entry.glucose, glucose > 38 else { continue }
-
-            let minutesAgo = mostRecentGlucoseDate.timeIntervalSince(entry.dateString) / 60
-            guard minutesAgo != 0 else { continue }
-            // compute mg/dL per 5 m as a Decimal:
-            let change = Decimal(mostRecentGlucoseReading - glucose)
-            let avgDelta = (change / Decimal(minutesAgo)) * Decimal(5)
-
-            // very-recent (<2.5 m) smooths "now"
-            if minutesAgo > -2, minutesAgo <= 2.5 {
-                mostRecentGlucoseReading = (mostRecentGlucoseReading + glucose) / 2
-                mostRecentGlucoseDate = Date(
-                    timeIntervalSince1970: (
-                        mostRecentGlucoseDate.timeIntervalSince1970 + entry.dateString
-                            .timeIntervalSince1970
-                    ) / 2
-                )
-            }
-            // short window (~5–15 m)
-            else if minutesAgo > 2.5, minutesAgo <= 17.5 {
-                shortDeltas.append(avgDelta)
-                if minutesAgo < 7.5 {
-                    lastDeltas.append(avgDelta)
-                }
-            }
-            // long window (~20–40 m)
-            else if minutesAgo > 17.5, minutesAgo < 42.5 {
-                longDeltas.append(avgDelta)
-            }
-        }
-
-        // compute means (or zero)
-        let lastDelta: Decimal = lastDeltas.mean
-        let shortAvg: Decimal = shortDeltas.mean
-        let longAvg: Decimal = longDeltas.mean
-
-        return GlucoseStatus(
-            delta: lastDelta.rounded(toPlaces: 2),
-            glucose: Decimal(mostRecentGlucoseReading),
-            noise: Int(sorted[0].noise ?? 0),
-            shortAvgDelta: shortAvg.rounded(toPlaces: 2),
-            longAvgDelta: longAvg.rounded(toPlaces: 2),
-            date: mostRecentGlucoseDate,
-            lastCalIndex: nil,
-            device: settingsManager.settings.cgm.rawValue
-        )
-    }
+    // FIXME: use this after we know oref-swift is good
+//    /// Fetches the most recent glucose readings from Core Data, filters and smooths them,
+//    /// and computes rolling delta statistics (last, short-term, and long-term).
+//    ///
+//    /// Mirrors JavaScript oref `glucose-get-last.js` logic.
+//    ///
+//    /// - Returns: A `GlucoseStatus` containing:
+//    ///   - `glucose`: the most recent glucose value (mg/dL),
+//    ///   - `delta`: the 5-minute delta (mg/dL per 5m),
+//    ///   - `shortAvgDelta`: the average delta over ~5–15 minutes,
+//    ///   - `longAvgDelta`: the average delta over ~20–40 minutes,
+//    ///   - `noise`: the CGM noise level (if any),
+//    ///   - `date`: the timestamp of the “now” reading,
+//    ///   - `lastCalIndex`: index of the last calibration record (always `nil` here),
+//    ///   - `device`: the source device string.
+//    ///
+//    /// - Throws: Any `CoreDataError` or other error encountered during fetch or context work.
+//    /// - Returns: `nil` if no valid glucose readings are found in the past day.
+//    public func getGlucoseStatus() async throws -> GlucoseStatus? {
+//        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+//            ofType: GlucoseStored.self,
+//            onContext: context,
+//            predicate: NSPredicate(
+//                format: "date >= %@ AND isManual == %@",
+//                Date.oneDayAgoInMinutes as NSDate,
+//                false as NSNumber
+//            ),
+//            key: "date",
+//            ascending: false
+//        )
+//
+//        guard let stored = results as? [GlucoseStored], !stored.isEmpty else {
+//            return nil
+//        }
+//
+//        let validReadings: [BloodGlucose] = await context.perform {
+//            stored.compactMap { entry in
+//                BloodGlucose(
+//                    _id: entry.id?.uuidString ?? UUID().uuidString,
+//                    sgv: Int(entry.glucose),
+//                    direction: BloodGlucose.Direction(from: entry.direction ?? ""),
+//                    date: Decimal(entry.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+//                    dateString: entry.date ?? Date(),
+//                    unfiltered: Decimal(entry.glucose),
+//                    filtered: Decimal(entry.glucose),
+//                    noise: nil,
+//                    glucose: Int(entry.glucose),
+//                    type: "sgv"
+//                )
+//            }
+//        }
+//
+//        guard !validReadings.isEmpty else {
+//            return nil
+//        }
+//
+//        // Sort descending (newest first)
+//        let sorted = validReadings.sorted { $0.date > $1.date }
+//
+//        let mostRecentGlucose = sorted[0]
+//        var mostRecentGlucoseReading: Int = mostRecentGlucose.glucose!
+//        var mostRecentGlucoseDate: Date = mostRecentGlucose.dateString
+//
+//        var lastDeltas: [Decimal] = []
+//        var shortDeltas: [Decimal] = []
+//        var longDeltas: [Decimal] = []
+//
+//        // Walk older entries to compute deltas
+//        for entry in sorted.dropFirst() {
+//            // JS oref has logic here around skipping calibration readings.
+//            // We never calibration record (never happens here, since type=="sgv")
+//            // so we omit this check
+//
+//            // only use readings >38 mg/dL (to skip code values, <39)
+//            guard let glucose = entry.glucose, glucose > 38 else { continue }
+//
+//            let minutesAgo = mostRecentGlucoseDate.timeIntervalSince(entry.dateString) / 60
+//            guard minutesAgo != 0 else { continue }
+//            // compute mg/dL per 5 m as a Decimal:
+//            let change = Decimal(mostRecentGlucoseReading - glucose)
+//            let avgDelta = (change / Decimal(minutesAgo)) * Decimal(5)
+//
+//            // very-recent (<2.5 m) smooths "now"
+//            if minutesAgo > -2, minutesAgo <= 2.5 {
+//                mostRecentGlucoseReading = (mostRecentGlucoseReading + glucose) / 2
+//                mostRecentGlucoseDate = Date(
+//                    timeIntervalSince1970: (
+//                        mostRecentGlucoseDate.timeIntervalSince1970 + entry.dateString
+//                            .timeIntervalSince1970
+//                    ) / 2
+//                )
+//            }
+//            // short window (~5–15 m)
+//            else if minutesAgo > 2.5, minutesAgo <= 17.5 {
+//                shortDeltas.append(avgDelta)
+//                if minutesAgo < 7.5 {
+//                    lastDeltas.append(avgDelta)
+//                }
+//            }
+//            // long window (~20–40 m)
+//            else if minutesAgo > 17.5, minutesAgo < 42.5 {
+//                longDeltas.append(avgDelta)
+//            }
+//        }
+//
+//        // compute means (or zero)
+//        let lastDelta: Decimal = lastDeltas.mean
+//        let shortAvg: Decimal = shortDeltas.mean
+//        let longAvg: Decimal = longDeltas.mean
+//
+//        return GlucoseStatus(
+//            delta: lastDelta.rounded(toPlaces: 2),
+//            glucose: Decimal(mostRecentGlucoseReading),
+//            noise: Int(sorted[0].noise ?? 0),
+//            shortAvgDelta: shortAvg.rounded(toPlaces: 2),
+//            longAvgDelta: longAvg.rounded(toPlaces: 2),
+//            date: mostRecentGlucoseDate,
+//            lastCalIndex: nil,
+//            device: settingsManager.settings.cgm.rawValue
+//        )
+//    }
 
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
         // Use injected context if available, otherwise create new task context

+ 4 - 2
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -91,7 +91,8 @@ import Testing
             javascriptDuration: 0.1,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         if comparison.resultType == .valueDifference {
@@ -129,7 +130,8 @@ import Testing
             javascriptDuration: 0.1,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         if comparison.resultType != .valueDifference {

+ 2 - 1
TrioTests/OpenAPSSwiftTests/MealJsonTests.swift

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

+ 2 - 1
TrioTests/OpenAPSSwiftTests/ProfileJavascriptTests.swift

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

+ 14 - 7
TrioTests/OpenAPSSwiftTests/ProfileJsNativeCompareTests.swift

@@ -97,7 +97,8 @@ import Testing
             javascriptDuration: 0.1,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -129,7 +130,8 @@ import Testing
             javascriptDuration: 0.2,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -150,7 +152,8 @@ import Testing
             javascriptDuration: 0.2,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .valueDifference)
@@ -173,7 +176,8 @@ import Testing
             javascriptDuration: 0.2,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .matchingExceptions)
@@ -193,7 +197,8 @@ import Testing
             javascriptDuration: 0.2,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .swiftOnlyException)
@@ -215,7 +220,8 @@ import Testing
             javascriptDuration: 0.2,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .jsOnlyException)
@@ -236,7 +242,8 @@ import Testing
             javascriptDuration: 0.2,
             iobInputs: nil,
             mealInputs: nil,
-            autosensInputs: nil
+            autosensInputs: nil,
+            determineBasalInputs: nil
         )
 
         #expect(comparison.resultType == .comparisonError)