ソースを参照

Fix triangle (ie remainingCI)

Sam King 10 ヶ月 前
コミット
23f4059d45

+ 4 - 4
Trio/Sources/APS/OpenAPSSwift/Forecasts/CarbImpactParams.swift

@@ -6,6 +6,7 @@ struct CarbImpactParams {
     let maxAbsorptionIntervals: Int
     let triangleIntervals: Int
     let remainingCarbImpactPeak: Decimal
+    let remainingCarbAbsorptionTime: Decimal
 
     static func calculate(
         carbSensitivityFactor: Decimal,
@@ -19,15 +20,13 @@ struct CarbImpactParams {
         let maxCarbImpact = (maxCarbAbsorptionRate * carbSensitivityFactor * 5 / 60).rounded(toPlaces: 1)
         let cappedCarbImpact = min(carbImpact, maxCarbImpact)
 
-        let computedRemainingCarbAbsorptionTime = ForecastGenerator.calculateRemainingCarbAbsorptionTime(
+        let remainingCarbAbsorptionTime = ForecastGenerator.calculateRemainingCarbAbsorptionTime(
             sensitivityRatio: sensitivityRatio,
             maxMealAbsorptionTime: profile.maxMealAbsorptionTime,
             mealCOB: mealData.mealCOB,
             lastCarbTime: Date(timeIntervalSince1970: mealData.lastCarbTime / 1000),
             currentTime: currentTime
         )
-        // Clamp remainingTime for more robustness
-        let remainingCarbAbsorptionTime = min(computedRemainingCarbAbsorptionTime, profile.maxMealAbsorptionTime)
 
         let carbImpactDuration: Decimal
         if carbImpact == 0 {
@@ -74,7 +73,8 @@ struct CarbImpactParams {
             carbImpactDuration: carbImpactDuration,
             maxAbsorptionIntervals: maxAbsorptionIntervals,
             triangleIntervals: triangleIntervals,
-            remainingCarbImpactPeak: remainingCarbImpactPeak
+            remainingCarbImpactPeak: remainingCarbImpactPeak,
+            remainingCarbAbsorptionTime: remainingCarbAbsorptionTime
         )
     }
 }

+ 8 - 17
Trio/Sources/APS/OpenAPSSwift/Forecasts/ForecastGenerator+Forecasts.swift

@@ -40,23 +40,14 @@ extension ForecastGenerator {
             // – ramp up linearly to peak over the first half of the window,
             // – ramp down linearly over the second half,
             // – zero afterwards.
-            let triangle: Decimal
-            if carbImpactParams.triangleIntervals > 0, result.count <= carbImpactParams.triangleIntervals {
-                // FIXME: integer division here might be slightly off for odd number intervals.
-                // FIXME: For perfect symmetry we could use let halfTriangle = (triangleIntervals + 1) / 2 — Change this?!
-                let halfTriangle = carbImpactParams.triangleIntervals / 2
-                if result.count <= halfTriangle {
-                    // Ramp up
-                    triangle = carbImpactParams.remainingCarbImpactPeak * Decimal(result.count) / Decimal(halfTriangle)
-                } else {
-                    // Ramp down
-                    triangle = carbImpactParams
-                        .remainingCarbImpactPeak * Decimal(carbImpactParams.triangleIntervals - result.count) /
-                        Decimal(halfTriangle)
-                }
-            } else {
-                triangle = 0
-            }
+
+            // var intervals = Math.min( COBpredBGs.length, (remainingCATime*12)-COBpredBGs.length );
+            // var remainingCI = Math.max(0, intervals / (remainingCATime/2*12) * remainingCIpeak );
+            let intervals = min(Decimal(result.count), carbImpactParams.remainingCarbAbsorptionTime * 12 - Decimal(result.count))
+            let triangle = max(
+                0,
+                intervals / (carbImpactParams.remainingCarbAbsorptionTime / 2 * 12) * carbImpactParams.remainingCarbImpactPeak
+            )
 
             let next = result.last!
                 + glucoseImpact.jsRounded(scale: 2)

+ 61 - 2
TrioTests/OpenAPSSwiftTests/DetermineBasalJsonTests.swift

@@ -97,13 +97,13 @@ import Testing
         #expect(comparison.resultType == .matching)
     }
 
-    @Test("Format determineBasal inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
+    @Test("Format determineBasal inputs for running in JS", .enabled(if: true)) 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/f1d04efa-c39b-4f0a-9955-65ab663ff9fb.0.json")
+        let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/0de8fde2-06ba-46e5-b810-990f77fbcfc4.2.json")
         let determineBasalInput = algorithmComparison.determineBasalInput!
 
         let encoder = JSONCoding.encoder
@@ -179,10 +179,69 @@ import Testing
 
         if comparison.resultType == .valueDifference {
             print(comparison.differences!.prettyPrintedJSON!)
+            printForecasts(comparison.differences)
         }
 
         #expect(comparison.resultType == .matching)
 
         timeZoneForTests.resetTimezone()
     }
+
+    func printForecasts(_ values: [String: Any]?) {
+        guard let values = values else { return }
+        guard let forecasts = values["predBGs"] as? Trio.ValueDifference else { return }
+        let js = forecasts.js.toDictionary()
+        let swift = forecasts.swift.toDictionary()
+
+        for forecastType in ["IOB", "ZT", "UAM", "COB"] {
+            let swiftForecast = swift[forecastType]!.toIntArray()
+            let jsForecast = js[forecastType]!.toIntArray()
+            print("")
+            if swiftForecast.count == jsForecast.count {
+                print(forecastType)
+            } else {
+                print("\(forecastType) has length mismatch ❌")
+            }
+            print("Row\tSft\tJS\tMatch")
+            print("--------------")
+            for (row, values) in zip(swiftForecast, jsForecast).enumerated() {
+                let pass: String
+                if abs(values.0 - values.1) <= 1 {
+                    pass = "✅"
+                } else {
+                    pass = "❌"
+                }
+                print("\(row)\t\(values.0)\t\(values.1)\t\(pass)")
+            }
+        }
+    }
+}
+
+extension JSONValue {
+    func toDictionary() -> [String: Trio.JSONValue] {
+        switch self {
+        case let .object(dict):
+            return dict
+        default:
+            fatalError()
+        }
+    }
+
+    func toIntArray() -> [Int] {
+        switch self {
+        case let .array(array):
+            return array.map { $0.toInt() }
+        default:
+            fatalError()
+        }
+    }
+
+    func toInt() -> Int {
+        switch self {
+        case let .number(number):
+            return Int(number)
+        default:
+            fatalError()
+        }
+    }
 }