Procházet zdrojové kódy

Major refactoring: fix calc of TB and scheduled basal

Deniz Cengiz před 1 rokem
rodič
revize
1f38033575
1 změnil soubory, kde provedl 167 přidání a 102 odebrání
  1. 167 102
      FreeAPS/Sources/APS/Storage/TDDStorage.swift

+ 167 - 102
FreeAPS/Sources/APS/Storage/TDDStorage.swift

@@ -41,26 +41,19 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         let pumpData = calculatePumpDataHours(pumpHistory)
         let pumpData = calculatePumpDataHours(pumpHistory)
         debug(.apsManager, "Hours of pump data available: \(pumpData)")
         debug(.apsManager, "Hours of pump data available: \(pumpData)")
 
 
-        if pumpData < 23.9, pumpData > 21 {
-            let missingHours = 24 - pumpData
-            debug(.apsManager, "Filling \(missingHours) missing hours with scheduled basals")
-            if let lastEntry = pumpHistory.last {
-                let endDate = lastEntry.timestamp
-                let calculatedGapStart = endDate.addingTimeInterval(-missingHours * 3600)
-                scheduledBasalInsulin = calculateScheduledBasalInsulin(
-                    from: calculatedGapStart,
-                    to: endDate,
-                    basalProfile: basalProfile,
-                    basalIncrement: basalIncrement
-                )
-                debug(.apsManager, "Added scheduled basal insulin: \(scheduledBasalInsulin)U")
-            }
+        let bolusEvents = pumpHistory.filter({ $0.type == .bolus })
+        let tempBasalEvents = pumpHistory.filter({ $0.type == .tempBasal })
+
+        let gaps = findBasalGaps(in: tempBasalEvents)
+
+        if !gaps.isEmpty {
+            scheduledBasalInsulin = calculateScheduledBasalInsulin(gaps: gaps, profile: basalProfile).rounded(toPlaces: 2)
         }
         }
 
 
-        bolusInsulin = calculateBolusInsulin(pumpHistory)
+        bolusInsulin = calculateBolusInsulin(bolusEvents).rounded(toPlaces: 2)
         debug(.apsManager, "Total bolus insulin: \(bolusInsulin)U")
         debug(.apsManager, "Total bolus insulin: \(bolusInsulin)U")
 
 
-        tempInsulin = calculateTempBasalInsulin(pumpHistory, basalIncrement: basalIncrement)
+        tempInsulin = calculateTempBasalInsulin(tempBasalEvents, basalIncrement: basalIncrement).rounded(toPlaces: 2)
         debug(.apsManager, "Total temp basal insulin: \(tempInsulin)U")
         debug(.apsManager, "Total temp basal insulin: \(tempInsulin)U")
 
 
         let total = bolusInsulin + tempInsulin + scheduledBasalInsulin
         let total = bolusInsulin + tempInsulin + scheduledBasalInsulin
@@ -72,7 +65,7 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         - Bolus: \(bolusInsulin)U (\((bolusInsulin / total * 100).rounded(toPlaces: 1))%)
         - Bolus: \(bolusInsulin)U (\((bolusInsulin / total * 100).rounded(toPlaces: 1))%)
         - Temp Basal: \(tempInsulin)U (\((tempInsulin / total * 100).rounded(toPlaces: 1))%)
         - Temp Basal: \(tempInsulin)U (\((tempInsulin / total * 100).rounded(toPlaces: 1))%)
         - Scheduled Basal: \(scheduledBasalInsulin)U (\((scheduledBasalInsulin / total * 100).rounded(toPlaces: 1))%)
         - Scheduled Basal: \(scheduledBasalInsulin)U (\((scheduledBasalInsulin / total * 100).rounded(toPlaces: 1))%)
-        - weightedAverage: \(weightedAverage ?? 0)
+        - WeightedAverage: \(weightedAverage ?? 0)
         - Hours of Data: \(pumpData)
         - Hours of Data: \(pumpData)
         """)
         """)
 
 
@@ -85,6 +78,60 @@ final class BaseTDDStorage: TDDStorage, Injectable {
             hoursOfData: pumpData
             hoursOfData: pumpData
         )
         )
     }
     }
+    
+    /// Finds gaps between tempBasal events where scheduled basal ran
+    /// - Parameter tempBasalEvents: Array of pump history events of type tempBasal
+    /// - Returns: Array of gaps, where each gap has a start and end time
+    private func findBasalGaps(in tempBasalEvents: [PumpHistoryEvent]) -> [(start: Date, end: Date)] {
+        guard !tempBasalEvents.isEmpty else {
+            // No events = full day gap
+            let startOfDay = Calendar.current.startOfDay(for: Date())
+            let endOfDay = startOfDay.addingTimeInterval(24 * 60 * 60 - 1)
+            return [(start: startOfDay, end: endOfDay)]
+        }
+
+        // Sort events by timestamp
+        let sortedEvents = tempBasalEvents.sorted { $0.timestamp < $1.timestamp }
+
+        var gaps: [(start: Date, end: Date)] = []
+
+        // Track the end time of the last temp basal event
+        var lastEndTime: Date?
+
+        for (index, event) in sortedEvents.enumerated() {
+            // Calculate the actual end time for the current event
+            guard let duration = event.duration else { continue }
+            var currentEndTime = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
+
+            // Check for a cancellation
+            if index < sortedEvents.count - 1 {
+                let nextEvent = sortedEvents[index + 1]
+                if nextEvent.timestamp < currentEndTime {
+                    // The next event cancels this one, adjust the end time
+                    currentEndTime = nextEvent.timestamp
+                }
+            }
+
+            // If there’s a gap between the last event's end time and the current event's start time, record it
+            if let lastEnd = lastEndTime, event.timestamp > lastEnd {
+                gaps.append((start: lastEnd, end: event.timestamp))
+            }
+
+            // Update the last end time to the current event's (possibly adjusted) end time
+            lastEndTime = currentEndTime
+        }
+
+        // Handle gap at the end of the dataset (if needed)
+        if let lastEnd = lastEndTime {
+            let endOfDay = Calendar.current.startOfDay(for: sortedEvents.first!.timestamp)
+                .addingTimeInterval(24 * 60 * 60 - 1)
+            if lastEnd < endOfDay {
+                gaps.append((start: lastEnd, end: endOfDay))
+            }
+        }
+
+        return gaps
+    }
 
 
     /// Calculates the number of hours of available pump history data
     /// Calculates the number of hours of available pump history data
     /// - Parameter pumpHistory: Array of pump history events
     /// - Parameter pumpHistory: Array of pump history events
@@ -108,69 +155,134 @@ final class BaseTDDStorage: TDDStorage, Injectable {
     }
     }
 
 
     /// Calculates total bolus insulin from pump history
     /// Calculates total bolus insulin from pump history
-    /// - Parameter pumpHistory: Array of pump history events
+    /// - Parameter bolusEvents: Array of pump history events of type bolus
     /// - Returns: Total bolus insulin
     /// - Returns: Total bolus insulin
-    private func calculateBolusInsulin(_ pumpHistory: [PumpHistoryEvent]) -> Decimal {
-        pumpHistory
-            .filter { $0.type == .bolus }
-            .reduce(Decimal(0)) { sum, event in
-                sum + (event.amount ?? 0)
+    private func calculateBolusInsulin(_ bolusEvents: [PumpHistoryEvent]) -> Decimal {
+        bolusEvents
+            .reduce(Decimal(0)) { totalBolusInsulin, event in
+                totalBolusInsulin + (event.amount ?? 0)
             }
             }
     }
     }
 
 
-    /// Calculates insulin delivered via temporary basal rates
-    /// - Parameter pumpHistory: Array of pump history events
+    /// Calculates insulin delivered via temporary basal rates, accounting for interruptions
+    /// - Parameters:
+    ///   - tempBasalEvents: Array of pump history events of type tempBasal
+    ///   - basalIncrement: The smallest increment for basal rates
     /// - Returns: Total temporary basal insulin
     /// - Returns: Total temporary basal insulin
-    private func calculateTempBasalInsulin(_ pumpHistory: [PumpHistoryEvent], basalIncrement: Decimal) -> Decimal {
-        var totalInsulin: Decimal = 0
+    private func calculateTempBasalInsulin(_ tempBasalEvents: [PumpHistoryEvent], basalIncrement: Decimal) -> Decimal {
+        guard !tempBasalEvents.isEmpty else { return Decimal(0) }
+
+        // Sort events by timestamp to ensure proper order
+        let sortedEvents = tempBasalEvents.sorted { $0.timestamp < $1.timestamp }
+
+        return sortedEvents.enumerated().reduce(Decimal(0)) { totalInsulin, currentEvent in
+            let (index, event) = currentEvent
+
+            // Ensure the event is of type tempBasal and has valid data
+            guard let rate = event.amount, // Rate in U/hr
+                  let durationMinutes = event.duration, // Duration in minutes
+                  rate > 0, durationMinutes > 0 else { return totalInsulin }
+
+            // Calculate the actual duration in minutes the temp basal ran
+            let actualDurationMinutes: Int
+            if index < sortedEvents.count - 1 {
+                // Next event exists; calculate if it interrupts the current event
+                let nextEvent = sortedEvents[index + 1]
+                let currentEndTime = event.timestamp.addingTimeInterval(TimeInterval(durationMinutes * 60))
+
+                if nextEvent.timestamp < currentEndTime {
+                    // Interrupted; calculate the actual duration
+                    let interruptedDuration = nextEvent.timestamp.timeIntervalSince(event.timestamp) / 60
+                    actualDurationMinutes = Int(interruptedDuration)
+                } else {
+                    // Not interrupted; use full duration
+                    actualDurationMinutes = durationMinutes
+                }
+            } else {
+                // Last event in the list; use its full duration
+                actualDurationMinutes = durationMinutes
+            }
 
 
-        for (index, event) in pumpHistory.enumerated() {
-            guard event.type == .tempBasal,
-                  let rate = event.amount,
-                  rate > 0,
-                  index > 0 else { continue }
+            // Convert the duration to hours and calculate insulin
+            let durationHours = Decimal(actualDurationMinutes) / 60
+            let insulin = accountForIncrements(rate * durationHours, basalIncrement: basalIncrement)
 
 
-            let duration = Decimal(pumpHistory[index - 1].duration ?? 0) / 60 // Convert to hours
-            let insulin = accountForIncrements(rate * duration, basalIncrement: basalIncrement)
-            totalInsulin += insulin
+            debug(
+                .apsManager,
+                "Temp basal: \(rate)U/hr for \(Decimal(actualDurationMinutes) / 60)hr = \(insulin)U (adjusted for interruptions if needed)"
+            )
 
 
-            debug(.apsManager, "Temp basal: \(rate)U/hr for \(duration)hr = \(insulin)U")
+            // Add the calculated insulin to the total
+            return totalInsulin + insulin
         }
         }
-
-        return totalInsulin
     }
     }
 
 
-    /// Calculates insulin delivered via scheduled basal rates
+    /// Calculates total scheduled basal insulin within gaps
     /// - Parameters:
     /// - Parameters:
-    ///   - from: Start date
-    ///   - to: End date
-    ///   - basalProfile: Array of basal profile entries
+    ///   - tempBasalEvents: Array of pump history events of type tempBasal
+    ///   - profile: Array of basal profile entries
     /// - Returns: Total scheduled basal insulin
     /// - Returns: Total scheduled basal insulin
     private func calculateScheduledBasalInsulin(
     private func calculateScheduledBasalInsulin(
-        from: Date,
-        to: Date,
-        basalProfile: [BasalProfileEntry],
-        basalIncrement: Decimal
+        gaps: [(start: Date, end: Date)],
+        profile: [BasalProfileEntry]
     ) -> Decimal {
     ) -> Decimal {
         var totalInsulin: Decimal = 0
         var totalInsulin: Decimal = 0
-        var currentDate = from
 
 
-        while currentDate < to {
-            let timeString = makeBaseString(from: currentDate)
-            guard let basalRate = findBasalRate(for: timeString, in: basalProfile) else { continue }
+        for gap in gaps {
+            var currentTime = gap.start
+
+            while currentTime < gap.end {
+                guard let rate = findBasalRate(for: getTimeString(from: currentTime), in: profile) else {
+                    debug(.apsManager, "No basal rate found for time \(currentTime)")
+                    break
+                }
 
 
-            let nextScheduleTime = findNextScheduleTime(after: timeString, in: basalProfile)
-            let durationInHours = calculateDuration(currentTime: timeString, nextScheduleTime: nextScheduleTime, endDate: to)
+                // Determine the next switch time in the basal profile or the end of the gap
+                let nextSwitchTime = getNextBasalRateSwitch(after: currentTime, in: profile) ?? gap.end
+                let endTime = min(nextSwitchTime, gap.end)
 
 
-            let insulin = accountForIncrements(basalRate * Decimal(durationInHours), basalIncrement: basalIncrement)
-            totalInsulin += insulin
+                // Calculate duration in hours and insulin delivered
+                let duration = Decimal(endTime.timeIntervalSince(currentTime)) / 3600
+                let insulin = rate * duration
+                totalInsulin += insulin
 
 
-            currentDate = currentDate.addingTimeInterval(durationInHours * 3600)
+                debug(.apsManager, "Scheduled basal: \(rate)U/hr from \(currentTime) to \(endTime) = \(insulin)U")
+
+                // Move to the next time block
+                currentTime = endTime
+            }
         }
         }
 
 
         return totalInsulin
         return totalInsulin
     }
     }
 
 
+    /// Finds the next basal profile switch after a given time
+    /// - Parameters:
+    ///   - time: Current time
+    ///   - profile: Array of basal profile entries
+    /// - Returns: The time of the next switch, if any
+    private func getNextBasalRateSwitch(after time: Date, in profile: [BasalProfileEntry]) -> Date? {
+        let calendar = Calendar.current
+        let timeMinutes = calendar.component(.hour, from: time) * 60 + calendar.component(.minute, from: time)
+
+        // Find the next entry in the profile after the current time
+        for entry in profile {
+            if entry.minutes > timeMinutes {
+                let nextSwitchTime = calendar.startOfDay(for: time).addingTimeInterval(TimeInterval(entry.minutes * 60))
+                return nextSwitchTime
+            }
+        }
+
+        return nil // No further switches; end of day
+    }
+
+    /// Converts a Date to a time string in "HH:mm:ss" format
+    private func getTimeString(from date: Date) -> String {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH:mm:ss"
+        return formatter.string(from: date)
+    }
+
     /// Rounds insulin amounts according to pump increment constraints
     /// Rounds insulin amounts according to pump increment constraints
     /// - Parameter insulin: Raw insulin amount
     /// - Parameter insulin: Raw insulin amount
     /// - Returns: Rounded insulin amount
     /// - Returns: Rounded insulin amount
@@ -196,15 +308,6 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         return 0
         return 0
     }
     }
 
 
-    /// Formats a date to time string in "HH:mm:ss" format
-    /// - Parameter date: Date to format
-    /// - Returns: Formatted time string
-    private func makeBaseString(from date: Date) -> String {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "HH:mm:ss"
-        return formatter.string(from: date)
-    }
-
     /// Finds the basal rate for a specific time in the profile, considering closest increments or wide coverage.
     /// Finds the basal rate for a specific time in the profile, considering closest increments or wide coverage.
     /// - Parameters:
     /// - Parameters:
     ///   - timeString: Time string in "HH:mm:ss" format
     ///   - timeString: Time string in "HH:mm:ss" format
@@ -236,44 +339,6 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         return nil
         return nil
     }
     }
 
 
-    /// Finds the next scheduled time in the basal profile
-    /// - Parameters:
-    ///   - time: Current time string
-    ///   - profile: Array of basal profile entries
-    /// - Returns: Next scheduled time
-    private func findNextScheduleTime(after time: String, in profile: [BasalProfileEntry]) -> String {
-        guard let currentIndex = profile.firstIndex(where: { $0.start == time }) else {
-            return profile[0].start
-        }
-
-        let nextIndex = (currentIndex + 1) % profile.count
-        return profile[nextIndex].start
-    }
-
-    /// Calculates duration between two schedule times
-    /// - Parameters:
-    ///   - currentTime: Current time string
-    ///   - nextScheduleTime: Next schedule time string
-    ///   - endDate: End date for calculations
-    /// - Returns: Duration in hours
-    private func calculateDuration(currentTime: String, nextScheduleTime: String, endDate _: Date) -> Double {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "HH:mm:ss"
-
-        guard let time1 = formatter.date(from: currentTime),
-              let time2 = formatter.date(from: nextScheduleTime)
-        else {
-            return 0
-        }
-
-        var difference = time2.timeIntervalSince(time1) / 3600
-        if difference < 0 {
-            difference += 24
-        }
-
-        return difference
-    }
-
     /// Calculates weighted average of TDD from historical data
     /// Calculates weighted average of TDD from historical data
     /// - Returns: Weighted average if available
     /// - Returns: Weighted average if available
     private func calculateWeightedAverage() -> Decimal? {
     private func calculateWeightedAverage() -> Decimal? {