LoopAlgorithm.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. //
  2. // LoopAlgorithm.swift
  3. // Learn
  4. //
  5. // Created by Pete Schwamb on 6/30/23.
  6. // Copyright © 2023 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. public enum AlgorithmError: Error {
  11. case missingGlucose
  12. case incompleteSchedules
  13. }
  14. public struct LoopAlgorithmEffects {
  15. public var insulin: [GlucoseEffect]
  16. public var carbs: [GlucoseEffect]
  17. public var retrospectiveCorrection: [GlucoseEffect]
  18. public var momentum: [GlucoseEffect]
  19. public var insulinCounteraction: [GlucoseEffectVelocity]
  20. }
  21. public struct AlgorithmEffectsOptions: OptionSet {
  22. public let rawValue: UInt8
  23. public static let carbs = AlgorithmEffectsOptions(rawValue: 1 << 0)
  24. public static let insulin = AlgorithmEffectsOptions(rawValue: 1 << 1)
  25. public static let momentum = AlgorithmEffectsOptions(rawValue: 1 << 2)
  26. public static let retrospection = AlgorithmEffectsOptions(rawValue: 1 << 3)
  27. public static let all: AlgorithmEffectsOptions = [.carbs, .insulin, .momentum, .retrospection]
  28. public init(rawValue: UInt8) {
  29. self.rawValue = rawValue
  30. }
  31. }
  32. public struct LoopPrediction: GlucosePrediction {
  33. public var glucose: [PredictedGlucoseValue]
  34. public var effects: LoopAlgorithmEffects
  35. }
  36. public struct DoseRecommendation: Equatable {
  37. public let basalAdjustment: TempBasalRecommendation?
  38. public let bolusUnits: Double?
  39. public init(basalAdjustment: TempBasalRecommendation?, bolusUnits: Double? = nil) {
  40. self.basalAdjustment = basalAdjustment
  41. self.bolusUnits = bolusUnits
  42. }
  43. }
  44. public actor LoopAlgorithm {
  45. public typealias InputType = LoopPredictionInput
  46. public typealias OutputType = LoopPrediction
  47. // public static func generateRecommendation(input: LoopAlgorithmInput) throws -> DoseRecommendation {
  48. // let prediction = try generatePrediction(input: input.predictionInput, startDate: input.predictionDate)
  49. //
  50. // switch input.doseRecommendationType {
  51. // case .manualBolus:
  52. // prediction.glucose.recommendedManualBolus(to: <#T##GlucoseRangeSchedule#>, suspendThreshold: <#T##HKQuantity?#>, sensitivity: <#T##InsulinSensitivitySchedule#>, model: <#T##InsulinModel#>, pendingInsulin: <#T##Double#>, maxBolus: <#T##Double#>)
  53. // case .automaticBolus:
  54. // <#code#>
  55. // case .tempBasal:
  56. // <#code#>
  57. // }
  58. // }
  59. // Generates a forecast predicting glucose.
  60. public static func generatePrediction(input: LoopPredictionInput, startDate: Date? = nil) throws -> LoopPrediction {
  61. guard let latestGlucose = input.glucoseHistory.last else {
  62. throw AlgorithmError.missingGlucose
  63. }
  64. let start = startDate ?? latestGlucose.startDate
  65. let insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: nil)
  66. let settings = input.settings
  67. if let doseStart = input.doses.first?.startDate {
  68. assert(!input.settings.basal.isEmpty, "Missing basal history input.")
  69. let basalStart = input.settings.basal.first!.startDate
  70. precondition(basalStart <= doseStart, "Basal history must cover historic dose range. First dose date: \(doseStart) < \(basalStart)")
  71. }
  72. // Overlay basal history on basal doses, splitting doses to get amount delivered relative to basal
  73. let annotatedDoses = input.doses.annotated(with: input.settings.basal)
  74. let insulinEffects = annotatedDoses.glucoseEffects(
  75. insulinModelProvider: insulinModelProvider,
  76. longestEffectDuration: settings.insulinActivityDuration,
  77. insulinSensitivityHistory: settings.sensitivity,
  78. from: start.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval).dateFlooredToTimeInterval(settings.delta),
  79. to: nil)
  80. // ICE
  81. let insulinCounteractionEffects = input.glucoseHistory.counteractionEffects(to: insulinEffects)
  82. // Carb Effects
  83. let carbEffects = input.carbEntries.map(
  84. to: insulinCounteractionEffects,
  85. carbRatio: settings.carbRatio,
  86. insulinSensitivity: settings.sensitivity
  87. ).dynamicGlucoseEffects(
  88. from: start.addingTimeInterval(-IntegralRetrospectiveCorrection.retrospectionInterval),
  89. carbRatios: settings.carbRatio,
  90. insulinSensitivities: settings.sensitivity
  91. )
  92. // RC
  93. let retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects)
  94. let retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies.combinedSums(of: LoopMath.retrospectiveCorrectionGroupingInterval * 1.01)
  95. let rc: RetrospectiveCorrection
  96. if input.settings.useIntegralRetrospectiveCorrection {
  97. rc = IntegralRetrospectiveCorrection(effectDuration: LoopMath.retrospectiveCorrectionEffectDuration)
  98. } else {
  99. rc = StandardRetrospectiveCorrection(effectDuration: LoopMath.retrospectiveCorrectionEffectDuration)
  100. }
  101. guard let curSensitivity = settings.sensitivity.closestPrior(to: start)?.value,
  102. let curBasal = settings.basal.closestPrior(to: start)?.value,
  103. let curTarget = settings.target.closestPrior(to: start)?.value else
  104. {
  105. throw AlgorithmError.incompleteSchedules
  106. }
  107. let rcEffect = rc.computeEffect(
  108. startingAt: latestGlucose,
  109. retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
  110. recencyInterval: TimeInterval(minutes: 15),
  111. insulinSensitivity: curSensitivity,
  112. basalRate: curBasal,
  113. correctionRange: curTarget,
  114. retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval
  115. )
  116. var effects = [[GlucoseEffect]]()
  117. if settings.algorithmEffectsOptions.contains(.carbs) {
  118. effects.append(carbEffects)
  119. }
  120. if settings.algorithmEffectsOptions.contains(.insulin) {
  121. effects.append(insulinEffects)
  122. }
  123. if settings.algorithmEffectsOptions.contains(.retrospection) {
  124. effects.append(rcEffect)
  125. }
  126. // Glucose Momentum
  127. let momentumEffects: [GlucoseEffect]
  128. if settings.algorithmEffectsOptions.contains(.momentum) {
  129. let momentumInputData = input.glucoseHistory.filterDateRange(start.addingTimeInterval(-GlucoseMath.momentumDataInterval), start)
  130. momentumEffects = momentumInputData.linearMomentumEffect()
  131. } else {
  132. momentumEffects = []
  133. }
  134. var prediction = LoopMath.predictGlucose(startingAt: latestGlucose, momentum: momentumEffects, effects: effects)
  135. // Dosing requires prediction entries at least as long as the insulin model duration.
  136. // If our prediction is shorter than that, then extend it here.
  137. let finalDate = latestGlucose.startDate.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration)
  138. if let last = prediction.last, last.startDate < finalDate {
  139. prediction.append(PredictedGlucoseValue(startDate: finalDate, quantity: last.quantity))
  140. }
  141. return LoopPrediction(
  142. glucose: prediction,
  143. effects: LoopAlgorithmEffects(
  144. insulin: insulinEffects,
  145. carbs: carbEffects,
  146. retrospectiveCorrection: rcEffect,
  147. momentum: momentumEffects,
  148. insulinCounteraction: insulinCounteractionEffects
  149. )
  150. )
  151. }
  152. }