IntegralRetrospectiveCorrection.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. //
  2. // IntegralRetrospectiveCorrection.swift
  3. // Loop
  4. //
  5. // Created by Dragan Maksimovic on 9/19/21.
  6. // Copyright © 2021 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. /**
  11. Integral Retrospective Correction (IRC) calculates a correction effect in glucose prediction based on a timeline of past discrepancies between observed glucose movement and movement expected based on insulin and carb models. Integral retrospective correction acts as a proportional-integral-differential (PID) controller aimed at reducing modeling errors in glucose prediction.
  12. In the above summary, "discrepancy" is a difference between the actual glucose and the model predicted glucose over retrospective correction grouping interval (set to 30 min in LoopSettings), whereas "past discrepancies" refers to a timeline of discrepancies computed over retrospective correction integration interval (set to 180 min in Loop Settings).
  13. */
  14. public class IntegralRetrospectiveCorrection: RetrospectiveCorrection {
  15. public static let retrospectionInterval = TimeInterval(minutes: 180)
  16. /// RetrospectiveCorrection protocol variables
  17. /// Standard effect duration
  18. let effectDuration: TimeInterval
  19. /// Overall retrospective correction effect
  20. public var totalGlucoseCorrectionEffect: HKQuantity?
  21. /**
  22. Integral retrospective correction parameters:
  23. - currentDiscrepancyGain: Standard retrospective correction gain
  24. - persistentDiscrepancyGain: Gain for persistent long-term modeling errors, must be greater than or equal to currentDiscrepancyGain
  25. - correctionTimeConstant: How fast integral effect accumulates in response to persistent errors
  26. - differentialGain: Differential effect gain
  27. - delta: Glucose sampling time interval (5 min)
  28. - maximumCorrectionEffectDuration: Maximum duration of the correction effect in glucose prediction
  29. - retrospectiveCorrectionIntegrationInterval: Maximum duration over which to integrate retrospective correction changes
  30. */
  31. static let currentDiscrepancyGain: Double = 1.0
  32. static let persistentDiscrepancyGain: Double = 2.0 // was 5.0
  33. static let correctionTimeConstant: TimeInterval = TimeInterval(minutes: 60.0) // was 90.0
  34. static let differentialGain: Double = 2.0
  35. static let delta: TimeInterval = TimeInterval(minutes: 5.0)
  36. static let maximumCorrectionEffectDuration: TimeInterval = TimeInterval(minutes: 180.0) // was 240.0
  37. /// Initialize computed integral retrospective correction parameters
  38. static let integralForget: Double = exp( -delta.minutes / correctionTimeConstant.minutes )
  39. static let integralGain: Double = ((1 - integralForget) / integralForget) *
  40. (persistentDiscrepancyGain - currentDiscrepancyGain)
  41. static let proportionalGain: Double = currentDiscrepancyGain - integralGain
  42. /// All math is performed with glucose expressed in mg/dL
  43. private let unit = HKUnit.milligramsPerDeciliter
  44. /// State variables reported in diagnostic issue report
  45. var recentDiscrepancyValues: [Double] = []
  46. var integralCorrectionEffectDuration: TimeInterval?
  47. var proportionalCorrection: Double = 0.0
  48. var integralCorrection: Double = 0.0
  49. var differentialCorrection: Double = 0.0
  50. var currentDate: Date = Date()
  51. var ircStatus: String = "-"
  52. /**
  53. Initialize integral retrospective correction settings based on current values of user settings
  54. - Parameters:
  55. - settings: User settings
  56. - insulinSensitivity: User insulin sensitivity schedule
  57. - basalRates: User basal rate schedule
  58. - Returns: Integral Retrospective Correction customized with controller parameters and user settings
  59. */
  60. public init(effectDuration: TimeInterval) {
  61. self.effectDuration = effectDuration
  62. }
  63. /**
  64. Calculates overall correction effect based on timeline of discrepancies, and updates glucoseCorrectionEffect
  65. - Parameters:
  66. - glucose: Most recent glucose
  67. - retrospectiveGlucoseDiscrepanciesSummed: Timeline of past discepancies
  68. - Returns:
  69. - totalRetrospectiveCorrection: Overall glucose effect
  70. */
  71. public func computeEffect(
  72. startingAt startingGlucose: GlucoseValue,
  73. retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?,
  74. recencyInterval: TimeInterval,
  75. insulinSensitivity: HKQuantity,
  76. basalRate: Double,
  77. correctionRange: ClosedRange<HKQuantity>,
  78. retrospectiveCorrectionGroupingInterval: TimeInterval
  79. ) -> [GlucoseEffect] {
  80. // Loop settings relevant for calculation of effect limits
  81. // let settings = UserDefaults.appGroup?.loopSettings ?? LoopSettings()
  82. currentDate = Date()
  83. // Last discrepancy should be recent, otherwise clear the effect and return
  84. let glucoseDate = startingGlucose.startDate
  85. var glucoseCorrectionEffect: [GlucoseEffect] = []
  86. guard let currentDiscrepancy = retrospectiveGlucoseDiscrepanciesSummed?.last,
  87. glucoseDate.timeIntervalSince(currentDiscrepancy.endDate) <= recencyInterval
  88. else {
  89. ircStatus = "discrepancy not available, effect not computed."
  90. totalGlucoseCorrectionEffect = nil
  91. return( [] )
  92. }
  93. // Default values if we are not able to calculate integral retrospective correction
  94. ircStatus = "defaulted to standard RC, past discrepancies or user settings not available."
  95. let currentDiscrepancyValue = currentDiscrepancy.quantity.doubleValue(for: unit)
  96. var scaledCorrection = currentDiscrepancyValue
  97. totalGlucoseCorrectionEffect = HKQuantity(unit: unit, doubleValue: currentDiscrepancyValue)
  98. integralCorrectionEffectDuration = effectDuration
  99. // Calculate integral retrospective correction if past discrepancies over integration interval are available and if user settings are available
  100. if let pastDiscrepancies = retrospectiveGlucoseDiscrepanciesSummed?.filterDateRange(glucoseDate.addingTimeInterval(-Self.retrospectionInterval), glucoseDate) {
  101. ircStatus = "effect computed successfully."
  102. // To reduce response delay, integral retrospective correction is computed over an array of recent contiguous discrepancy values having the same sign as the latest discrepancy value
  103. recentDiscrepancyValues = []
  104. var nextDiscrepancy = currentDiscrepancy
  105. let currentDiscrepancySign = currentDiscrepancy.quantity.doubleValue(for: unit).sign
  106. for pastDiscrepancy in pastDiscrepancies.reversed() {
  107. let pastDiscrepancyValue = pastDiscrepancy.quantity.doubleValue(for: unit)
  108. if (pastDiscrepancyValue.sign == currentDiscrepancySign &&
  109. nextDiscrepancy.endDate.timeIntervalSince(pastDiscrepancy.endDate)
  110. <= recencyInterval && abs(pastDiscrepancyValue) >= 0.1)
  111. {
  112. recentDiscrepancyValues.append(pastDiscrepancyValue)
  113. nextDiscrepancy = pastDiscrepancy
  114. } else {
  115. break
  116. }
  117. }
  118. recentDiscrepancyValues = recentDiscrepancyValues.reversed()
  119. let correctionRangeMin = correctionRange.lowerBound.doubleValue(for: unit)
  120. let correctionRangeMax = correctionRange.upperBound.doubleValue(for: unit)
  121. let latestGlucoseValue = startingGlucose.quantity.doubleValue(for: unit) // most recent glucose
  122. // Safety limit for (+) integral effect. The limit is set to a larger value if the current blood glucose is further away from the correction range because we have more time available for corrections
  123. let glucoseError = latestGlucoseValue - correctionRangeMax
  124. let zeroTempEffect = abs(insulinSensitivity.doubleValue(for: unit) * basalRate)
  125. let integralEffectPositiveLimit = min(max(glucoseError, 1.0 * zeroTempEffect), 4.0 * zeroTempEffect)
  126. // Limit for (-) integral effect: glucose prediction reduced by no more than 10 mg/dL below the correction range minimum
  127. let integralEffectNegativeLimit = -max(10.0, latestGlucoseValue - correctionRangeMin)
  128. // Integral effect math
  129. integralCorrection = 0.0
  130. var integralCorrectionEffectMinutes = effectDuration.minutes - 2.0 * IntegralRetrospectiveCorrection.delta.minutes
  131. for discrepancy in recentDiscrepancyValues {
  132. integralCorrection =
  133. IntegralRetrospectiveCorrection.integralForget * integralCorrection +
  134. IntegralRetrospectiveCorrection.integralGain * discrepancy
  135. integralCorrectionEffectMinutes += 2.0 * IntegralRetrospectiveCorrection.delta.minutes
  136. }
  137. // Limits applied to integral correction effect and effect duration
  138. integralCorrection = min(max(integralCorrection, integralEffectNegativeLimit), integralEffectPositiveLimit)
  139. integralCorrectionEffectMinutes = min(integralCorrectionEffectMinutes, IntegralRetrospectiveCorrection.maximumCorrectionEffectDuration.minutes)
  140. // Differential effect math
  141. var differentialDiscrepancy: Double = 0.0
  142. if recentDiscrepancyValues.count > 1 {
  143. let previousDiscrepancyValue = recentDiscrepancyValues[recentDiscrepancyValues.count - 2]
  144. differentialDiscrepancy = currentDiscrepancyValue - previousDiscrepancyValue
  145. }
  146. // Overall glucose effect calculated as a sum of propotional, integral and differential effects
  147. proportionalCorrection = IntegralRetrospectiveCorrection.proportionalGain * currentDiscrepancyValue
  148. // Differential effect added only when negative, to avoid upward stacking with momentum, while still mitigating sluggishness of retrospective correction when discrepancies start decreasing
  149. if differentialDiscrepancy < 0.0 {
  150. differentialCorrection = IntegralRetrospectiveCorrection.differentialGain * differentialDiscrepancy
  151. } else {
  152. differentialCorrection = 0.0
  153. }
  154. let totalCorrection = proportionalCorrection + integralCorrection + differentialCorrection
  155. totalGlucoseCorrectionEffect = HKQuantity(unit: unit, doubleValue: totalCorrection)
  156. integralCorrectionEffectDuration = TimeInterval(minutes: integralCorrectionEffectMinutes)
  157. // correction value scaled to account for extended effect duration
  158. scaledCorrection = totalCorrection * effectDuration.minutes / integralCorrectionEffectDuration!.minutes
  159. }
  160. let retrospectionTimeInterval = currentDiscrepancy.endDate.timeIntervalSince(currentDiscrepancy.startDate)
  161. let discrepancyTime = max(retrospectionTimeInterval, retrospectiveCorrectionGroupingInterval)
  162. let velocity = HKQuantity(unit: unit.unitDivided(by: .second()), doubleValue: scaledCorrection / discrepancyTime)
  163. // Update array of glucose correction effects
  164. glucoseCorrectionEffect = startingGlucose.decayEffect(atRate: velocity, for: integralCorrectionEffectDuration!)
  165. // Return glucose correction effects
  166. return( glucoseCorrectionEffect )
  167. }
  168. public var debugDescription: String {
  169. let report: [String] = [
  170. "## IntegralRetrospectiveCorrection",
  171. "",
  172. "Last updated: \(currentDate)",
  173. "Status: \(ircStatus)",
  174. "currentDiscrepancyGain: \(IntegralRetrospectiveCorrection.currentDiscrepancyGain)",
  175. "persistentDiscrepancyGain: \(IntegralRetrospectiveCorrection.persistentDiscrepancyGain)",
  176. "correctionTimeConstant [min]: \(IntegralRetrospectiveCorrection.correctionTimeConstant.minutes)",
  177. "proportionalGain: \(IntegralRetrospectiveCorrection.proportionalGain)",
  178. "integralForget: \(IntegralRetrospectiveCorrection.integralForget)",
  179. "integralGain: \(IntegralRetrospectiveCorrection.integralGain)",
  180. "differentialGain: \(IntegralRetrospectiveCorrection.differentialGain)",
  181. "Integration performed over \(recentDiscrepancyValues.count) most recent discrepancies having the same sign as the latest discrepancy value. Earliest-to-most-recent recentDiscrepancyValues [mg/dL]: \(recentDiscrepancyValues)",
  182. "proportionalCorrection [mg/dL]: \(proportionalCorrection)",
  183. "integralCorrection [mg/dL]: \(integralCorrection)",
  184. "differentialCorrection [mg/dL]: \(differentialCorrection)",
  185. "totalGlucoseCorrectionEffect: \(String(describing: totalGlucoseCorrectionEffect))",
  186. "integralCorrectionEffectDuration [min]: \(String(describing: integralCorrectionEffectDuration?.minutes))"
  187. ]
  188. return report.joined(separator: "\n")
  189. }
  190. }