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

Address PR feedback
* Replace indices shenanigans
* Refactor to lower runtime complexity

Deniz Cengiz 2 месяцев назад
Родитель
Сommit
8a575044c6
1 измененных файлов с 91 добавлено и 75 удалено
  1. 91 75
      Trio/Sources/APS/FetchGlucoseManager.swift

+ 91 - 75
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -244,9 +244,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
+            // Predicate must cover at least the full glucose horizon used by downstream algorithm consumers.
+            // If autosens / oref / smoothing logic ever starts looking back further (e.g. 36h),
+            // this fetch window must be expanded accordingly.
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             key: "date",
-            ascending: false,
+            ascending: true, // fetch newest -> oldest
             fetchLimit: 350
         ) as? [GlucoseStored]
     }
@@ -353,16 +356,23 @@ extension CGMManager {
 }
 
 extension BaseFetchGlucoseManager: SettingsObserver {
-    /// Smooth glucose data when smoothing is turned on
+    /// Smooth glucose data when smoothing is turned on.
     func settingsDidChange(_: TrioSettings) {
-        if settingsManager.settings.smoothGlucose, !shouldSmoothGlucose {
-            glucoseStoreAndHeartLock.wait()
+        let smoothingWasEnabled = shouldSmoothGlucose
+        let smoothingIsEnabled = settingsManager.settings.smoothGlucose
+        shouldSmoothGlucose = smoothingIsEnabled
+
+        guard smoothingIsEnabled, !smoothingWasEnabled else { return }
+
+        processQueue.async { [weak self] in
+            guard let self else { return }
+
+            self.glucoseStoreAndHeartLock.wait()
             Task {
-                await self.exponentialSmoothingGlucose(context: context)
-                glucoseStoreAndHeartLock.signal()
+                await self.exponentialSmoothingGlucose(context: self.context)
+                self.glucoseStoreAndHeartLock.signal()
             }
         }
-        shouldSmoothGlucose = settingsManager.settings.smoothGlucose
     }
 }
 
@@ -377,15 +387,16 @@ extension BaseFetchGlucoseManager {
 
         await context.perform {
             // Only smooth CGM values; ignore manually entered glucose.
-            // `fetchGlucose(context:)` already returns newest first, and filtering preserves order.
-            let cgmValuesNewestFirst: [GlucoseStored] = glucoseStored
+            // `fetchGlucose(context:)` returns chronological order (oldest -> newest),
+            // which matches the natural order required by the smoothing algorithm.
+            let glucoseReadings: [GlucoseStored] = glucoseStored
                 .filter { !$0.isManual }
                 .filter { $0.date != nil }
 
-            guard !cgmValuesNewestFirst.isEmpty else { return }
+            guard !glucoseReadings.isEmpty else { return }
 
             self.applyExponentialSmoothingAndStore(
-                newestFirst: cgmValuesNewestFirst,
+                glucoseReadings: glucoseReadings,
                 minimumWindowSize: 4,
                 maximumAllowedGapMinutes: 12,
                 xDripErrorGlucose: 38,
@@ -408,7 +419,7 @@ extension BaseFetchGlucoseManager {
     }
 
     private func applyExponentialSmoothingAndStore(
-        newestFirst data: [GlucoseStored],
+        glucoseReadings data: [GlucoseStored],
         minimumWindowSize: Int,
         maximumAllowedGapMinutes: Int,
         xDripErrorGlucose: Int,
@@ -418,100 +429,105 @@ extension BaseFetchGlucoseManager {
         secondOrderAlpha: Decimal,
         secondOrderBeta: Decimal
     ) {
-        let recordCount = data.count
-        guard recordCount > 0 else { return }
+        guard !data.isEmpty else { return }
 
-        // We need i+1 access while scanning gaps -> initial validWindowCount must be <= count-1
-        var validWindowCount = max(recordCount - 1, 0)
+        // Determine the size of the valid most-recent smoothing window.
+        // We walk adjacent pairs from newest -> oldest to preserve the same window semantics
+        // as the original implementation, but avoid manual reverse indexing.
+        var validWindowCount = max(data.count - 1, 0)
 
-        // Trim window based on rounded minute gaps or xDrip error value (38)
-        if validWindowCount > 0 {
-            for i in 0 ..< validWindowCount {
-                guard let newerDate = data[i].date, let olderDate = data[i + 1].date else { continue }
+        for (recentOffset, pair) in zip(data.dropFirst().reversed(), data.dropLast().reversed()).enumerated() {
+            let (newer, older) = pair
 
-                let gapSeconds = newerDate.timeIntervalSince(olderDate)
-                let gapMinutesRounded = Int((gapSeconds / 60.0).rounded()) // Kotlin: round(...)
+            guard let newerDate = newer.date, let olderDate = older.date else { continue }
 
-                if gapMinutesRounded >= maximumAllowedGapMinutes {
-                    validWindowCount = i + 1 // include the more recent reading
-                    break
-                }
+            let gapSeconds = newerDate.timeIntervalSince(olderDate)
+            let gapMinutesRounded = Int((gapSeconds / 60.0).rounded())
 
-                // possible FIXME: we probably do not need this; ported none the less from AAPS
-                if Int(data[i].glucose) == xDripErrorGlucose {
-                    validWindowCount = i // exclude this 38 value
-                    break
-                }
+            if gapMinutesRounded >= maximumAllowedGapMinutes {
+                validWindowCount = recentOffset + 1 // include the more recent reading
+                break
+            }
+
+            // Ported from AAPS: 38 mg/dL may represent an xDrip error state.
+            if Int(newer.glucose) == xDripErrorGlucose {
+                validWindowCount = recentOffset // exclude this 38 value
+                break
             }
         }
 
-        // If insufficient valid readings: copy raw into smoothed (clamped) for all passed entries
+        // If insufficient valid readings: copy raw into smoothed (clamped) for all passed entries.
         guard validWindowCount >= minimumWindowSize else {
-            for obj in data {
-                let raw = Decimal(Int(obj.glucose))
-                obj.smoothedGlucose = max(raw, minimumSmoothedGlucose) as NSDecimalNumber
+            for object in data {
+                let raw = Decimal(Int(object.glucose))
+                object.smoothedGlucose = max(raw, minimumSmoothedGlucose) as NSDecimalNumber
             }
             return
         }
 
-        // ---- 1st order smoothing (newest-first arrays, Kotlin add(0, ...) equivalent) ----
+        // Restrict smoothing to the valid most-recent window, still in chronological order.
+        let validWindow = data.suffix(validWindowCount)
+
+        guard let oldest = validWindow.first else { return }
+
+        // ---- 1st order smoothing ----
         var firstOrderSmoothed: [Decimal] = []
-        firstOrderSmoothed.reserveCapacity(validWindowCount + 1)
+        firstOrderSmoothed.reserveCapacity(validWindow.count)
 
-        // Initialize with the oldest valid point (index validWindowCount - 1)
-        firstOrderSmoothed = [Decimal(Int(data[validWindowCount - 1].glucose))]
+        var firstOrderCurrent = Decimal(Int(oldest.glucose))
+        firstOrderSmoothed.append(firstOrderCurrent)
 
-        for i in 0 ..< validWindowCount {
-            let raw = Decimal(Int(data[validWindowCount - 1 - i].glucose))
-            let prev = firstOrderSmoothed[0]
-            let next = prev + firstOrderAlpha * (raw - prev)
-            firstOrderSmoothed.insert(next, at: 0)
+        for sample in validWindow.dropFirst() {
+            let raw = Decimal(Int(sample.glucose))
+            firstOrderCurrent = firstOrderCurrent + firstOrderAlpha * (raw - firstOrderCurrent)
+            firstOrderSmoothed.append(firstOrderCurrent)
         }
 
         // ---- 2nd order smoothing ----
+        let secondOrderInput = Array(validWindow)
+        guard secondOrderInput.count >= 2 else { return }
+
         var secondOrderSmoothed: [Decimal] = []
-        var secondOrderDelta: [Decimal] = []
-        secondOrderSmoothed.reserveCapacity(validWindowCount)
-        secondOrderDelta.reserveCapacity(validWindowCount)
+        secondOrderSmoothed.reserveCapacity(secondOrderInput.count)
 
-        secondOrderSmoothed = [Decimal(Int(data[validWindowCount - 1].glucose))]
-        secondOrderDelta = [
-            Decimal(Int(data[validWindowCount - 2].glucose) - Int(data[validWindowCount - 1].glucose))
-        ]
+        var secondOrderDeltas: [Decimal] = []
+        secondOrderDeltas.reserveCapacity(secondOrderInput.count)
 
-        for i in 0 ..< (validWindowCount - 1) {
-            let raw = Decimal(Int(data[validWindowCount - 2 - i].glucose))
+        var previousSecondOrderSmoothed = Decimal(Int(secondOrderInput[0].glucose))
+        var previousSecondOrderDelta =
+            Decimal(Int(secondOrderInput[1].glucose) - Int(secondOrderInput[0].glucose))
 
-            let sBG = secondOrderSmoothed[0]
-            let sD = secondOrderDelta[0]
+        secondOrderSmoothed.append(previousSecondOrderSmoothed)
+        secondOrderDeltas.append(previousSecondOrderDelta)
 
-            let nextBG = secondOrderAlpha * raw + (1 - secondOrderAlpha) * (sBG + sD)
-            secondOrderSmoothed.insert(nextBG, at: 0)
+        for sample in secondOrderInput.dropFirst() {
+            let raw = Decimal(Int(sample.glucose))
 
-            let nextD =
-                secondOrderBeta * (secondOrderSmoothed[0] - secondOrderSmoothed[1])
-                    + (1 - secondOrderBeta) * secondOrderDelta[0]
-            secondOrderDelta.insert(nextD, at: 0)
+            let nextSmoothed =
+                secondOrderAlpha * raw
+                    + (1 - secondOrderAlpha) * (previousSecondOrderSmoothed + previousSecondOrderDelta)
+
+            let nextDelta =
+                secondOrderBeta * (nextSmoothed - previousSecondOrderSmoothed)
+                    + (1 - secondOrderBeta) * previousSecondOrderDelta
+
+            previousSecondOrderSmoothed = nextSmoothed
+            previousSecondOrderDelta = nextDelta
+
+            secondOrderSmoothed.append(nextSmoothed)
+            secondOrderDeltas.append(nextDelta)
         }
 
         // ---- Weighted blend ----
-        var blended: [Decimal] = []
-        blended.reserveCapacity(secondOrderSmoothed.count)
-
-        for i in secondOrderSmoothed.indices {
-            let value =
-                firstOrderWeight * firstOrderSmoothed[i]
-                    + (1 - firstOrderWeight) * secondOrderSmoothed[i]
-            blended.append(value)
+        let blended = zip(firstOrderSmoothed, secondOrderSmoothed).map { firstOrder, secondOrder in
+            firstOrderWeight * firstOrder + (1 - firstOrderWeight) * secondOrder
         }
 
-        // Apply to the most recent `limit` readings (same behavior as Kotlin)
-        let limit = min(blended.count, data.count)
-        for i in 0 ..< limit {
-            let rounded = blended[i].rounded(toPlaces: 0) // nearest integer, ties away from zero
+        // Apply to the most recent valid-window readings.
+        for (object, blendedValue) in zip(validWindow, blended) {
+            let rounded = blendedValue.rounded(toPlaces: 0) // nearest integer, ties away from zero
             let clamped = max(rounded, minimumSmoothedGlucose)
-
-            data[i].smoothedGlucose = clamped as NSDecimalNumber
+            object.smoothedGlucose = clamped as NSDecimalNumber
         }
     }
 }