GuardrailTests.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. //
  2. // GuardrailTests.swift
  3. // GuardrailTests
  4. //
  5. // Created by Michael Pangburn on 7/30/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. @testable import LoopKit
  11. class GuardrailTests: XCTestCase {
  12. let correctionRangeSchedule120 = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(120...130))])
  13. let preMealTargetRange120 = DoubleRange(120...130).quantityRange(for: .milligramsPerDeciliter)
  14. let workoutTargetRange120 = DoubleRange(120...130).quantityRange(for: .milligramsPerDeciliter)
  15. let correctionRangeSchedule80 = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(80...100))])
  16. let preMealTargetRange85 = DoubleRange(85...100).quantityRange(for: .milligramsPerDeciliter)
  17. let workoutTargetRange90 = DoubleRange(90...100).quantityRange(for: .milligramsPerDeciliter)
  18. func testSuspendThresholdUnits() {
  19. XCTAssertTrue(Guardrail.suspendThreshold.absoluteBounds.contains(HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 67)))
  20. XCTAssertTrue(Guardrail.suspendThreshold.absoluteBounds.contains(HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 110)))
  21. XCTAssertTrue(Guardrail.suspendThreshold.absoluteBounds.contains(HKQuantity(unit: .millimolesPerLiter, doubleValue: 6.1)))
  22. XCTAssertTrue(Guardrail.suspendThreshold.recommendedBounds.contains(HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 74)))
  23. XCTAssertTrue(Guardrail.suspendThreshold.recommendedBounds.contains(HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)))
  24. XCTAssertTrue(Guardrail.suspendThreshold.absoluteBounds.contains(HKQuantity(unit: .millimolesPerLiter, doubleValue: 4.44)))
  25. }
  26. func testMaxSuspensionThresholdValue() {
  27. let correctionRangeInputs = [ nil, correctionRangeSchedule120, correctionRangeSchedule80 ]
  28. let preMealInputs = [ nil, preMealTargetRange120, preMealTargetRange85 ]
  29. let workoutInputs = [ nil, workoutTargetRange120, workoutTargetRange90 ]
  30. let expected: [Double] = [ 110, 110, 90,
  31. 110, 110, 90,
  32. 85, 85, 85,
  33. 110, 110, 90,
  34. 110, 110, 90,
  35. 85, 85, 85,
  36. 80, 80, 80,
  37. 80, 80, 80,
  38. 80, 80, 80 ]
  39. var index = 0
  40. for correctionRange in correctionRangeInputs {
  41. for preMeal in preMealInputs {
  42. for workout in workoutInputs {
  43. let maxSuspendThresholdValue = Guardrail.maxSuspendThresholdValue(correctionRangeSchedule: correctionRange, preMealTargetRange: preMeal, workoutTargetRange: workout).doubleValue(for: .milligramsPerDeciliter)
  44. XCTAssertEqual(expected[index], maxSuspendThresholdValue, "Index \(index) failed")
  45. index += 1
  46. }
  47. }
  48. }
  49. }
  50. func testMinCorrectionRangeValue() {
  51. let suspendThresholdInputs: [Double?] = [ nil, 80, 88 ]
  52. let expected: [Double] = [ 87, 87, 88 ]
  53. for (index, suspendThreshold) in suspendThresholdInputs.enumerated() {
  54. XCTAssertEqual(expected[index], Guardrail.minCorrectionRangeValue(suspendThreshold: suspendThreshold.map { GlucoseThreshold(unit: .milligramsPerDeciliter, value: $0) }).doubleValue(for: .milligramsPerDeciliter), "Index \(index) failed")
  55. }
  56. }
  57. func testCorrectionRange() {
  58. let guardrail = Guardrail.correctionRange
  59. let expectedAndTest: [(SafetyClassification, Double)] = [
  60. (SafetyClassification.withinRecommendedRange, 100),
  61. (SafetyClassification.withinRecommendedRange, 115),
  62. (SafetyClassification.outsideRecommendedRange(.belowRecommended), 100.nextDown),
  63. (SafetyClassification.outsideRecommendedRange(.aboveRecommended), 115.nextUp),
  64. (SafetyClassification.outsideRecommendedRange(.maximum), 180),
  65. (SafetyClassification.outsideRecommendedRange(.minimum), 87),
  66. ]
  67. for test in expectedAndTest {
  68. XCTAssertEqual(test.0, guardrail.classification(for: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: test.1)), "for \(test.1)")
  69. }
  70. }
  71. func testWorkoutCorrectionRange() {
  72. let correctionRangeInputs = [ 70...80, 70...85, 70...90 ]
  73. let suspendThresholdInputs: [Double?] = [ nil, 81, 91 ]
  74. let expectedLow: [Double] = [ 85, 85, 91,
  75. 85, 85, 91,
  76. 90, 90, 91 ]
  77. let expectedMin: [Double] = [ 85, 85, 91, 85, 85, 91, 85, 85, 91 ]
  78. var index = 0
  79. for correctionRange in correctionRangeInputs {
  80. for suspendThreshold in suspendThresholdInputs {
  81. let guardrail = Guardrail.correctionRangeOverride(for: .workout, correctionRangeScheduleRange: correctionRange.range(withUnit: .milligramsPerDeciliter), suspendThreshold: suspendThreshold.map { GlucoseThreshold(unit: .milligramsPerDeciliter, value: $0) })
  82. XCTAssertEqual(expectedLow[index], guardrail.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter), "Index \(index) failed")
  83. XCTAssertEqual(expectedMin[index], guardrail.absoluteBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter), "Index \(index) failed")
  84. index += 1
  85. }
  86. }
  87. }
  88. func testPreMealCorrectionRange() {
  89. let correctionRangeInputs = [ 60...80, 100...110, 150...180 ]
  90. let suspendThresholdInputs: [Double?] = [ nil, 90 ]
  91. let expectedRecommendedHigh: [Double] = [ 67, 90,
  92. 100, 100,
  93. 130, 130 ]
  94. let expectedMin: [Double] = [ 67, 90, 67, 90, 67, 90 ]
  95. var index = 0
  96. for correctionRange in correctionRangeInputs {
  97. for suspendThreshold in suspendThresholdInputs {
  98. let guardrail = Guardrail.correctionRangeOverride(for: .preMeal, correctionRangeScheduleRange: correctionRange.range(withUnit: .milligramsPerDeciliter), suspendThreshold: suspendThreshold.map { GlucoseThreshold(unit: .milligramsPerDeciliter, value: $0) })
  99. XCTAssertEqual(expectedRecommendedHigh[index], guardrail.recommendedBounds.upperBound.doubleValue(for: .milligramsPerDeciliter), "Index \(index) failed")
  100. XCTAssertEqual(expectedMin[index], guardrail.absoluteBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter), "Index \(index) failed")
  101. XCTAssertEqual(guardrail.absoluteBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter),
  102. guardrail.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter), "Index \(index) failed")
  103. index += 1
  104. }
  105. }
  106. }
  107. func testCarbRatioGuardrail() {
  108. XCTAssertEqual(2.0...150.0, Guardrail.carbRatio.absoluteBounds.range(withUnit: .gramsPerUnit))
  109. XCTAssertEqual(4...28, Guardrail.carbRatio.recommendedBounds.range(withUnit: .gramsPerUnit))
  110. }
  111. func testBasalRateGuardrail() {
  112. let supportedBasalRates = (2...600).map { Double($0) / 20 }
  113. let guardrail = Guardrail.basalRate(supportedBasalRates: supportedBasalRates)
  114. XCTAssertEqual(0.1...30.0, guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour))
  115. XCTAssertEqual(0.1...30.0, guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour))
  116. }
  117. func testBasalRateGuardrailClampedLow() {
  118. let supportedBasalRates = [0.01, 1.0, 30.0]
  119. let guardrail = Guardrail.basalRate(supportedBasalRates: supportedBasalRates)
  120. XCTAssertEqual(1.0...30.0, guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour))
  121. XCTAssertEqual(1.0...30.0, guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour))
  122. }
  123. func testBasalRateGuardrailClampedHigh() {
  124. let supportedBasalRates = (2...800).map { Double($0) / 20 }
  125. let guardrail = Guardrail.basalRate(supportedBasalRates: supportedBasalRates)
  126. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.1...30.0)
  127. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 0.1...30.0)
  128. }
  129. func testBasalRateGuardrailZeroDropsFirst() {
  130. let supportedBasalRates = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
  131. let guardrail = Guardrail.basalRate(supportedBasalRates: supportedBasalRates)
  132. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 1.0...5.0)
  133. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 1.0...5.0)
  134. }
  135. func testMaxBasalRateGuardrail() {
  136. let supportedBasalRates = (1...600).map { Double($0) / 20 }
  137. let scheduledBasalRange = 0.05...0.78125
  138. let lowestCarbRatio = 10.0
  139. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  140. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.78125...7.0)
  141. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 1.6...5.0)
  142. }
  143. func testMaxBasalRateGuardrailHighCarbRatio() {
  144. let supportedBasalRates = (1...600).map { Double($0) / 20 }
  145. let scheduledBasalRange = 0.05...0.78125
  146. let lowestCarbRatio = 150.0
  147. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  148. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.78125...5.0)
  149. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 1.6...5.0)
  150. }
  151. func testMaxBasalRateGuardrailHigherCarbRatioClampsRecommendedBounds() {
  152. let supportedBasalRates = (1...600).map { Double($0) / 20 }
  153. let scheduledBasalRange = 0.05...0.78125
  154. let lowestCarbRatio = 15.0
  155. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  156. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.78125...5.0)
  157. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 1.6...5.0)
  158. }
  159. func testMaxBasalRateGuardrailNoCarbRatio() {
  160. let supportedBasalRates = (1...600).map { Double($0) / 20 }
  161. let scheduledBasalRange = 0.05...0.78125
  162. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: nil)
  163. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.78125...30.0)
  164. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 1.6...5.0)
  165. }
  166. func testMaxBasalRateGuardrailFewSupportedBasalRates() {
  167. let supportedBasalRates = [0.05, 1.0]
  168. let scheduledBasalRange = 0.05...0.78125
  169. let lowestCarbRatio = 10.0
  170. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  171. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.78125...1.0)
  172. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 1.0...1.0)
  173. }
  174. func testMaxBasalRateGuardrailHighestScheduledBasalZero() {
  175. let supportedBasalRates = [0.0, 1.0]
  176. let scheduledBasalRange = 0.0...0.0
  177. let lowestCarbRatio = 10.0
  178. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  179. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.0...1.0)
  180. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 0.0...0.0)
  181. }
  182. func testMaxBasalRateGuardrailNoScheduledBasalRates() {
  183. let supportedBasalRates = [0, 0.05, 1.0]
  184. let lowestCarbRatio = 10.0
  185. let guardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: nil, lowestCarbRatio: lowestCarbRatio)
  186. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnitsPerHour), 0.05...1.0)
  187. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnitsPerHour), 0.05...1.0)
  188. }
  189. func testSelectableBasalRatesGuardrail() {
  190. let supportedBasalRates = [0, 0.05, 1.0]
  191. let scheduledBasalRange = 0.05...0.78125
  192. let lowestCarbRatio = 10.0
  193. let selectableMaxBasalRates = Guardrail.selectableMaxBasalRates(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
  194. XCTAssertEqual([1.0], selectableMaxBasalRates)
  195. }
  196. func testSelectableBasalRatesGuardrailNoScheduledBasalRates() {
  197. let supportedBasalRates = [0, 0.05, 1.0]
  198. let lowestCarbRatio = 10.0
  199. let selectableMaxBasalRates = Guardrail.selectableMaxBasalRates(supportedBasalRates: supportedBasalRates, scheduledBasalRange: nil, lowestCarbRatio: lowestCarbRatio)
  200. XCTAssertEqual([0.05, 1.0], selectableMaxBasalRates)
  201. }
  202. func testMaxBolusGuardrailInsideLimits() {
  203. let supportedBolusVolumes = [0.05, 1.0, 2.0]
  204. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  205. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnit()), 0.05...2.0)
  206. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnit()), 1.0...2.0)
  207. }
  208. func testMaxBolusGuardrailClamped() {
  209. let supportedBolusVolumes = [0.05, 1.0, 2.0, 20.0.nextDown, 20.0, 25.0, 30.0, 30.0.nextUp]
  210. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  211. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnit()), 0.05...30.0)
  212. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnit()), 1.0...20.0.nextDown)
  213. }
  214. func testMaxBolusGuardrailDropsZeroVolume() {
  215. let supportedBolusVolumes = [0.0, 0.05, 1.0, 2.0, 20.0.nextDown, 20.0, 25.0, 30.0, 30.0.nextUp]
  216. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  217. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnit()), 0.05...30.0)
  218. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnit()), 1.0...20.0.nextDown)
  219. }
  220. func testMaxBolusGuardrailDropsAllZeroVolumes() {
  221. let supportedBolusVolumes = [0.0, 0.0, 0.05, 1.0, 2.0, 20.0.nextDown, 20.0, 25.0, 30.0, 30.0.nextUp]
  222. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  223. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnit()), 0.05...30.0)
  224. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnit()), 1.0...20.0.nextDown)
  225. }
  226. func testMaxBolusGuardrailDropsNegatives() {
  227. let supportedBolusVolumes = [-2.0, -1.0, 0.05, 1.0, 2.0, 20.0.nextDown, 20.0, 25.0, 30.0, 30.0.nextUp]
  228. let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
  229. XCTAssertEqual(guardrail.absoluteBounds.range(withUnit: .internationalUnit()), 0.05...30.0)
  230. XCTAssertEqual(guardrail.recommendedBounds.range(withUnit: .internationalUnit()), 1.0...20.0.nextDown)
  231. }
  232. func testSelectableBolusVolumes() {
  233. let supportedBolusVolumes = [0.0, 0.05, 1.0, 2.0, 30.nextUp]
  234. let selectableBolusVolumes = Guardrail.selectableBolusVolumes(supportedBolusVolumes: supportedBolusVolumes)
  235. XCTAssertEqual([0.05, 1.0, 2.0], selectableBolusVolumes)
  236. }
  237. }
  238. fileprivate extension ClosedRange where Bound == HKQuantity {
  239. func range(withUnit unit: HKUnit) -> ClosedRange<Double> {
  240. lowerBound.doubleValue(for: unit)...upperBound.doubleValue(for: unit)
  241. }
  242. }
  243. fileprivate extension ClosedRange where Bound == Int {
  244. func range(withUnit unit: HKUnit) -> ClosedRange<HKQuantity> {
  245. HKQuantity(unit: unit, doubleValue: Double(lowerBound))...HKQuantity(unit: unit, doubleValue: Double(upperBound))
  246. }
  247. }