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

Move core IoB loop from Decimal to Double

This commit converts one of the core internal IoB calculations from
using Decimal for numbers to Double. By using Double we give up on
precision but add speed due to hardware acceleration for floating
point math. The use of Double is confined to this internal algorithm,
the rest of the code that calls this function still operates on
Doubles.
Sam King 2 месяцев назад
Родитель
Сommit
1dde20ec08

+ 28 - 16
Trio/Sources/APS/OpenAPSSwift/Iob/IobCalculation.swift

@@ -12,8 +12,8 @@ struct IobTotal: Codable {
 
 
 enum IobCalculation {
 enum IobCalculation {
     struct IobCalculationResult {
     struct IobCalculationResult {
-        let activityContrib: Decimal
-        let iobContrib: Decimal
+        let activityContrib: Double
+        let iobContrib: Double
     }
     }
 
 
     /// logic to look up insulinPeakTime, taking into account `useCustomPeakTime`
     /// logic to look up insulinPeakTime, taking into account `useCustomPeakTime`
@@ -65,7 +65,19 @@ enum IobCalculation {
         let iobContrib = insulin *
         let iobContrib = insulin *
             (1 - S * (1 - a) * ((pow(minsAgo, 2) / (tau * end * (1 - a)) - minsAgo / tau - 1) * exp(-minsAgo / tau) + 1))
             (1 - S * (1 - a) * ((pow(minsAgo, 2) / (tau * end * (1 - a)) - minsAgo / tau - 1) * exp(-minsAgo / tau) + 1))
 
 
-        return IobCalculationResult(activityContrib: Decimal(activityContrib), iobContrib: Decimal(iobContrib))
+        guard activityContrib.isFinite, iobContrib.isFinite else {
+            return IobCalculationResult(activityContrib: 0, iobContrib: 0)
+        }
+
+        return IobCalculationResult(activityContrib: activityContrib, iobContrib: iobContrib)
+    }
+
+    /// Round a Double using the same logic as Decimal.jsRounded(scale:):
+    /// floor(value * 10^scale + 0.5) / 10^scale
+    private static func jsRound(_ value: Double, scale: Int) -> Decimal {
+        guard value.isFinite else { return 0 }
+        let multiplier = pow(10.0, Double(scale))
+        return Decimal((value * multiplier + 0.5).rounded(.down) / multiplier)
     }
     }
 
 
     static func iobTotal(treatments: [ComputedPumpHistoryEvent], profile: Profile, time now: Date) throws -> IobTotal {
     static func iobTotal(treatments: [ComputedPumpHistoryEvent], profile: Profile, time now: Date) throws -> IobTotal {
@@ -73,12 +85,12 @@ enum IobCalculation {
             throw IobError.diaNotSet
             throw IobError.diaNotSet
         }
         }
 
 
-        var iob = Decimal(0)
-        var basaliob = Decimal(0)
-        var bolusiob = Decimal(0)
-        var netbasalinsulin = Decimal(0)
-        var bolusinsulin = Decimal(0)
-        var activity = Decimal(0)
+        var iob = 0.0
+        var basaliob = 0.0
+        var bolusiob = 0.0
+        var netbasalinsulin = 0.0
+        var bolusinsulin = 0.0
+        var activity = 0.0
 
 
         if dia < 5 {
         if dia < 5 {
             dia = 5
             dia = 5
@@ -88,7 +100,7 @@ enum IobCalculation {
         let treatments = treatments.filter({ $0.timestamp <= now && $0.timestamp > diaAgo })
         let treatments = treatments.filter({ $0.timestamp <= now && $0.timestamp > diaAgo })
         for treatment in treatments {
         for treatment in treatments {
             guard let tIOB = try iobCalc(treatment: treatment, time: now, dia: dia, profile: profile),
             guard let tIOB = try iobCalc(treatment: treatment, time: now, dia: dia, profile: profile),
-                  let insulin = treatment.insulin
+                  let insulin = treatment.insulin.map({ Double($0) })
             else {
             else {
                 continue
                 continue
             }
             }
@@ -107,12 +119,12 @@ enum IobCalculation {
         }
         }
 
 
         return IobTotal(
         return IobTotal(
-            iob: iob.jsRounded(scale: 3),
-            activity: activity.jsRounded(scale: 4),
-            basaliob: basaliob.jsRounded(scale: 3),
-            bolusiob: bolusiob.jsRounded(scale: 3),
-            netbasalinsulin: netbasalinsulin.jsRounded(scale: 3),
-            bolusinsulin: bolusinsulin.jsRounded(scale: 3),
+            iob: jsRound(iob, scale: 3),
+            activity: jsRound(activity, scale: 4),
+            basaliob: jsRound(basaliob, scale: 3),
+            bolusiob: jsRound(bolusiob, scale: 3),
+            netbasalinsulin: jsRound(netbasalinsulin, scale: 3),
+            bolusinsulin: jsRound(bolusinsulin, scale: 3),
             time: now
             time: now
         )
         )
     }
     }

+ 6 - 6
TrioTests/OpenAPSSwiftTests/IobCalculateTests.swift

@@ -55,8 +55,8 @@ import Testing
         )
         )
 
 
         #expect(result != nil)
         #expect(result != nil)
-        #expect(result!.activityContrib.isWithin(0.0001, of: 0.0115))
-        #expect(result!.iobContrib.isWithin(0.0001, of: 1.8085))
+        #expect(result!.activityContrib.isApproximatelyEqual(to: 0.0115, epsilon: 0.0001))
+        #expect(result!.iobContrib.isApproximatelyEqual(to: 1.8085, epsilon: 0.0001))
     }
     }
 
 
     @Test("should calculate IOB with custom peak time for rapid-acting insulin") func calculateCustomPeakRapidActing() async throws {
     @Test("should calculate IOB with custom peak time for rapid-acting insulin") func calculateCustomPeakRapidActing() async throws {
@@ -78,8 +78,8 @@ import Testing
         )
         )
 
 
         #expect(result != nil)
         #expect(result != nil)
-        #expect(result!.activityContrib.isWithin(0.0001, of: 0.0079))
-        #expect(result!.iobContrib.isWithin(0.0001, of: 1.8763))
+        #expect(result!.activityContrib.isApproximatelyEqual(to: 0.0079, epsilon: 0.0001))
+        #expect(result!.iobContrib.isApproximatelyEqual(to: 1.8763, epsilon: 0.0001))
     }
     }
 
 
     @Test("should handle peak time limits for rapid-acting insulin") func handlePeakTimeLimitsRapidActing() async throws {
     @Test("should handle peak time limits for rapid-acting insulin") func handlePeakTimeLimitsRapidActing() async throws {
@@ -131,8 +131,8 @@ import Testing
         )
         )
 
 
         #expect(result != nil)
         #expect(result != nil)
-        #expect(result!.activityContrib.isWithin(0.0001, of: 0.01569))
-        #expect(result!.iobContrib.isWithin(0.0001, of: 1.7202))
+        #expect(result!.activityContrib.isApproximatelyEqual(to: 0.01569, epsilon: 0.0001))
+        #expect(result!.iobContrib.isApproximatelyEqual(to: 1.7202, epsilon: 0.0001))
     }
     }
 
 
     @Test("should handle peak time limits for ultra-rapid insulin") func handlePeakTimeLimitsUltraRapid() async throws {
     @Test("should handle peak time limits for ultra-rapid insulin") func handlePeakTimeLimitsUltraRapid() async throws {