Guardrail+Settings.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. //
  2. // Guardrail+Settings.swift
  3. // LoopKit
  4. //
  5. // Created by Rick Pasetto on 7/14/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. public extension Guardrail where Value == HKQuantity {
  10. static let suspendThreshold = Guardrail(absoluteBounds: 67...110, recommendedBounds: 74...80, unit: .milligramsPerDeciliter, startingSuggestion: 80)
  11. static func maxSuspendThresholdValue(correctionRangeSchedule: GlucoseRangeSchedule?, preMealTargetRange: ClosedRange<HKQuantity>?, workoutTargetRange: ClosedRange<HKQuantity>?) -> HKQuantity {
  12. return [
  13. suspendThreshold.absoluteBounds.upperBound,
  14. correctionRangeSchedule?.minLowerBound(),
  15. preMealTargetRange?.lowerBound,
  16. workoutTargetRange?.lowerBound
  17. ]
  18. .compactMap { $0 }
  19. .min()!
  20. }
  21. static let correctionRange = Guardrail(absoluteBounds: 87...180, recommendedBounds: 100...115, unit: .milligramsPerDeciliter, startingSuggestion: 100)
  22. static func minCorrectionRangeValue(suspendThreshold: GlucoseThreshold?) -> HKQuantity {
  23. return [
  24. correctionRange.absoluteBounds.lowerBound,
  25. suspendThreshold?.quantity
  26. ]
  27. .compactMap { $0 }
  28. .max()!
  29. }
  30. // Static "unconstrained" constant values before applying constraints
  31. static let unconstrainedWorkoutCorrectionRange = Guardrail(
  32. absoluteBounds: correctionRange.absoluteBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...250,
  33. recommendedBounds: correctionRange.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...180,
  34. unit: .milligramsPerDeciliter)
  35. fileprivate static func workoutCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
  36. suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
  37. let absoluteLowerBound = [
  38. unconstrainedWorkoutCorrectionRange.absoluteBounds.lowerBound,
  39. suspendThreshold?.quantity
  40. ]
  41. .compactMap { $0 }
  42. .max()!
  43. let recommmendedLowerBound = max(absoluteLowerBound, correctionRangeScheduleRange.upperBound)
  44. return Guardrail(
  45. absoluteBounds: absoluteLowerBound...unconstrainedWorkoutCorrectionRange.absoluteBounds.upperBound,
  46. recommendedBounds: recommmendedLowerBound...unconstrainedWorkoutCorrectionRange.recommendedBounds.upperBound
  47. )
  48. }
  49. static let premealCorrectionRangeMaximum = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 130.0)
  50. fileprivate static func preMealCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
  51. suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
  52. let absoluteLowerBound = suspendThreshold?.quantity ?? Guardrail.suspendThreshold.absoluteBounds.lowerBound
  53. return Guardrail(
  54. absoluteBounds: absoluteLowerBound...premealCorrectionRangeMaximum,
  55. recommendedBounds: absoluteLowerBound...min(max(absoluteLowerBound, correctionRangeScheduleRange.lowerBound), premealCorrectionRangeMaximum)
  56. )
  57. }
  58. static func correctionRangeOverride(for preset: CorrectionRangeOverrides.Preset,
  59. correctionRangeScheduleRange: ClosedRange<HKQuantity>,
  60. suspendThreshold: GlucoseThreshold?) -> Guardrail {
  61. switch preset {
  62. case .workout:
  63. return workoutCorrectionRange(correctionRangeScheduleRange: correctionRangeScheduleRange, suspendThreshold: suspendThreshold)
  64. case .preMeal:
  65. return preMealCorrectionRange(correctionRangeScheduleRange: correctionRangeScheduleRange, suspendThreshold: suspendThreshold)
  66. }
  67. }
  68. static let insulinSensitivity = Guardrail(
  69. absoluteBounds: 10...500,
  70. recommendedBounds: 16...399,
  71. unit: HKUnit.milligramsPerDeciliter.unitDivided(by: .internationalUnit()),
  72. startingSuggestion: 50
  73. )
  74. static let carbRatio = Guardrail(
  75. absoluteBounds: 2...150,
  76. recommendedBounds: 4...28,
  77. unit: .gramsPerUnit,
  78. startingSuggestion: 15
  79. )
  80. static func basalRate(supportedBasalRates: [Double]) -> Guardrail {
  81. let scheduledBasalRateAbsoluteRange = 0.0...30.0
  82. let allowedBasalRates = supportedBasalRates.filter { scheduledBasalRateAbsoluteRange.contains($0) }
  83. return Guardrail(
  84. absoluteBounds: allowedBasalRates.first!...allowedBasalRates.last!,
  85. recommendedBounds: allowedBasalRates.first!...allowedBasalRates.last!,
  86. unit: .internationalUnitsPerHour,
  87. startingSuggestion: allowedBasalRates.first!
  88. )
  89. }
  90. static func maximumBasalRate(
  91. supportedBasalRates: [Double],
  92. scheduledBasalRange: ClosedRange<Double>?,
  93. lowestCarbRatio: Double?,
  94. maximumBasalRatePrecision decimalPlaces: Int = 3
  95. ) -> Guardrail {
  96. let maximumUpperBound = 70.0 / (lowestCarbRatio ?? carbRatio.absoluteBounds.lowerBound.doubleValue(for: .gramsPerUnit))
  97. let absoluteUpperBound = maximumUpperBound.matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
  98. let recommendedHighScheduledBasalScaleFactor = 6.4
  99. let recommendedLowScheduledBasalScaleFactor = 2.1
  100. let recommendedLowerBound: Double
  101. let recommendedUpperBound: Double
  102. if let highestScheduledBasalRate = scheduledBasalRange?.upperBound {
  103. recommendedLowerBound = (recommendedLowScheduledBasalScaleFactor * highestScheduledBasalRate).matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
  104. recommendedUpperBound = (recommendedHighScheduledBasalScaleFactor * highestScheduledBasalRate).matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
  105. let absoluteBounds = highestScheduledBasalRate...max(absoluteUpperBound, recommendedUpperBound)
  106. let recommendedBounds = (recommendedLowerBound...recommendedUpperBound).clamped(to: absoluteBounds)
  107. return Guardrail(
  108. absoluteBounds: absoluteBounds,
  109. recommendedBounds: recommendedBounds,
  110. unit: .internationalUnitsPerHour
  111. )
  112. } else {
  113. let bounds = supportedBasalRates.drop { $0 <= 0 }.first!...absoluteUpperBound
  114. return Guardrail(
  115. absoluteBounds: bounds,
  116. recommendedBounds: bounds,
  117. unit: .internationalUnitsPerHour,
  118. startingSuggestion: 3.clamped(to: bounds)
  119. )
  120. }
  121. }
  122. static func selectableMaxBasalRates(supportedBasalRates: [Double],
  123. scheduledBasalRange: ClosedRange<Double>?,
  124. lowestCarbRatio: Double?,
  125. maximumBasalRatePrecision decimalPlaces: Int = 3) -> [Double] {
  126. let basalGuardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  127. let maximumScheduledBasalRate = scheduledBasalRange?.upperBound ?? -Double.infinity
  128. return supportedBasalRates
  129. .drop { $0 < maximumScheduledBasalRate }
  130. .filter { basalGuardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0)) }
  131. }
  132. static func maximumBolus(supportedBolusVolumes: [Double]) -> Guardrail {
  133. let maxBolusThresholdUnits: Double = 30
  134. let maxBolusWarningThresholdUnits: Double = 20
  135. let supportedBolusVolumes = supportedBolusVolumes.filter { $0 > 0 && $0 <= maxBolusThresholdUnits }
  136. let recommendedUpperBound = supportedBolusVolumes.last { $0 < maxBolusWarningThresholdUnits }
  137. let recommendedBounds = supportedBolusVolumes.dropFirst().first!...recommendedUpperBound!
  138. return Guardrail(
  139. absoluteBounds: supportedBolusVolumes.first!...supportedBolusVolumes.last!,
  140. recommendedBounds: recommendedBounds,
  141. unit: .internationalUnit(),
  142. startingSuggestion: 5.clamped(to: recommendedBounds)
  143. )
  144. }
  145. static func selectableBolusVolumes(supportedBolusVolumes: [Double]) -> [Double] {
  146. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  147. return supportedBolusVolumes.filter {
  148. guardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnit(), doubleValue: $0))
  149. }
  150. }
  151. }