Guardrail+Settings.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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(absoluteBounds: 85...250,
  32. recommendedBounds: correctionRange.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...180,
  33. unit: .milligramsPerDeciliter)
  34. fileprivate static func workoutCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
  35. suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
  36. let absoluteLowerBound = [
  37. unconstrainedWorkoutCorrectionRange.absoluteBounds.lowerBound,
  38. suspendThreshold?.quantity
  39. ]
  40. .compactMap { $0 }
  41. .max()!
  42. let recommmendedLowerBound = max(absoluteLowerBound, correctionRangeScheduleRange.upperBound)
  43. return Guardrail(
  44. absoluteBounds: absoluteLowerBound...unconstrainedWorkoutCorrectionRange.absoluteBounds.upperBound,
  45. recommendedBounds: recommmendedLowerBound...unconstrainedWorkoutCorrectionRange.recommendedBounds.upperBound
  46. )
  47. }
  48. static let premealCorrectionRangeMaximum = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 130.0)
  49. fileprivate static func preMealCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
  50. suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
  51. let absoluteLowerBound = suspendThreshold?.quantity ?? Guardrail.suspendThreshold.absoluteBounds.lowerBound
  52. return Guardrail(
  53. absoluteBounds: absoluteLowerBound...premealCorrectionRangeMaximum,
  54. recommendedBounds: absoluteLowerBound...min(max(absoluteLowerBound, correctionRangeScheduleRange.lowerBound), premealCorrectionRangeMaximum)
  55. )
  56. }
  57. static func correctionRangeOverride(for preset: CorrectionRangeOverrides.Preset,
  58. correctionRangeScheduleRange: ClosedRange<HKQuantity>,
  59. suspendThreshold: GlucoseThreshold?) -> Guardrail {
  60. switch preset {
  61. case .workout:
  62. return workoutCorrectionRange(correctionRangeScheduleRange: correctionRangeScheduleRange, suspendThreshold: suspendThreshold)
  63. case .preMeal:
  64. return preMealCorrectionRange(correctionRangeScheduleRange: correctionRangeScheduleRange, suspendThreshold: suspendThreshold)
  65. }
  66. }
  67. static let insulinSensitivity = Guardrail(
  68. absoluteBounds: 10...500,
  69. recommendedBounds: 16...399,
  70. unit: HKUnit.milligramsPerDeciliter.unitDivided(by: .internationalUnit()),
  71. startingSuggestion: 50
  72. )
  73. static let carbRatio = Guardrail(
  74. absoluteBounds: 2...150,
  75. recommendedBounds: 4...28,
  76. unit: .gramsPerUnit,
  77. startingSuggestion: 15
  78. )
  79. static func basalRate(supportedBasalRates: [Double]) -> Guardrail {
  80. let scheduledBasalRateAbsoluteRange = 0.05...30.0
  81. let allowedBasalRates = supportedBasalRates.filter { scheduledBasalRateAbsoluteRange.contains($0) }
  82. return Guardrail(
  83. absoluteBounds: allowedBasalRates.first!...allowedBasalRates.last!,
  84. recommendedBounds: allowedBasalRates.first!...allowedBasalRates.last!,
  85. unit: .internationalUnitsPerHour,
  86. startingSuggestion: allowedBasalRates.first!
  87. )
  88. }
  89. static func maximumBasalRate(
  90. supportedBasalRates: [Double],
  91. scheduledBasalRange: ClosedRange<Double>?,
  92. lowestCarbRatio: Double?,
  93. maximumBasalRatePrecision decimalPlaces: Int = 3
  94. ) -> Guardrail {
  95. let maximumUpperBound = 70.0 / (lowestCarbRatio ?? carbRatio.absoluteBounds.lowerBound.doubleValue(for: .gramsPerUnit))
  96. let absoluteUpperBound = maximumUpperBound.matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
  97. let recommendedHighScheduledBasalScaleFactor = 6.4
  98. let recommendedLowScheduledBasalScaleFactor = 2.1
  99. let recommendedLowerBound: Double
  100. let recommendedUpperBound: Double
  101. if let highestScheduledBasalRate = scheduledBasalRange?.upperBound {
  102. recommendedLowerBound = (recommendedLowScheduledBasalScaleFactor * highestScheduledBasalRate).matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
  103. recommendedUpperBound = (recommendedHighScheduledBasalScaleFactor * highestScheduledBasalRate).matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
  104. let absoluteBounds = highestScheduledBasalRate...max(absoluteUpperBound, recommendedUpperBound)
  105. let recommendedBounds = (recommendedLowerBound...recommendedUpperBound).clamped(to: absoluteBounds)
  106. return Guardrail(
  107. absoluteBounds: absoluteBounds,
  108. recommendedBounds: recommendedBounds,
  109. unit: .internationalUnitsPerHour
  110. )
  111. } else {
  112. let bounds = supportedBasalRates.drop { $0 <= 0 }.first!...absoluteUpperBound
  113. return Guardrail(
  114. absoluteBounds: bounds,
  115. recommendedBounds: bounds,
  116. unit: .internationalUnitsPerHour,
  117. startingSuggestion: 3.clamped(to: bounds)
  118. )
  119. }
  120. }
  121. static func selectableMaxBasalRates(supportedBasalRates: [Double],
  122. scheduledBasalRange: ClosedRange<Double>?,
  123. lowestCarbRatio: Double?,
  124. maximumBasalRatePrecision decimalPlaces: Int = 3) -> [Double] {
  125. let basalGuardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  126. let maximumScheduledBasalRate = scheduledBasalRange?.upperBound ?? -Double.infinity
  127. return supportedBasalRates
  128. .drop { $0 < maximumScheduledBasalRate }
  129. .filter { basalGuardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0)) }
  130. }
  131. static func maximumBolus(supportedBolusVolumes: [Double]) -> Guardrail {
  132. let maxBolusThresholdUnits: Double = 30
  133. let maxBolusWarningThresholdUnits: Double = 20
  134. let supportedBolusVolumes = supportedBolusVolumes.filter { $0 > 0 && $0 <= maxBolusThresholdUnits }
  135. let recommendedUpperBound = supportedBolusVolumes.last { $0 < maxBolusWarningThresholdUnits }
  136. let recommendedBounds = supportedBolusVolumes.dropFirst().first!...recommendedUpperBound!
  137. return Guardrail(
  138. absoluteBounds: supportedBolusVolumes.first!...supportedBolusVolumes.last!,
  139. recommendedBounds: recommendedBounds,
  140. unit: .internationalUnit(),
  141. startingSuggestion: 5.clamped(to: recommendedBounds)
  142. )
  143. }
  144. static func selectableBolusVolumes(supportedBolusVolumes: [Double]) -> [Double] {
  145. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  146. return supportedBolusVolumes.filter {
  147. guardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnit(), doubleValue: $0))
  148. }
  149. }
  150. }