DoseMath.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. //
  2. // DoseMath.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 3/8/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. private enum InsulinCorrection {
  11. case inRange
  12. case aboveRange(min: GlucoseValue, correcting: GlucoseValue, minTarget: HKQuantity, units: Double)
  13. case entirelyBelowRange(min: GlucoseValue, minTarget: HKQuantity, units: Double)
  14. case suspend(min: GlucoseValue)
  15. }
  16. extension InsulinCorrection {
  17. /// The delivery units for the correction
  18. private var units: Double {
  19. switch self {
  20. case .aboveRange(min: _, correcting: _, minTarget: _, units: let units):
  21. return units
  22. case .entirelyBelowRange(min: _, minTarget: _, units: let units):
  23. return units
  24. case .inRange, .suspend:
  25. return 0
  26. }
  27. }
  28. /// Determines the temp basal over `duration` needed to perform the correction.
  29. ///
  30. /// - Parameters:
  31. /// - scheduledBasalRate: The scheduled basal rate at the time the correction is delivered
  32. /// - maxBasalRate: The maximum allowed basal rate
  33. /// - duration: The duration of the temporary basal
  34. /// - rateRounder: The smallest fraction of a unit supported in basal delivery
  35. /// - Returns: A temp basal recommendation
  36. fileprivate func asTempBasal(
  37. scheduledBasalRate: Double,
  38. maxBasalRate: Double,
  39. duration: TimeInterval,
  40. rateRounder: ((Double) -> Double)?
  41. ) -> TempBasalRecommendation {
  42. var rate = units / (duration / TimeInterval(hours: 1)) // units/hour
  43. switch self {
  44. case .aboveRange, .inRange, .entirelyBelowRange:
  45. rate += scheduledBasalRate
  46. case .suspend:
  47. break
  48. }
  49. rate = Swift.min(maxBasalRate, Swift.max(0, rate))
  50. rate = rateRounder?(rate) ?? rate
  51. return TempBasalRecommendation(
  52. unitsPerHour: rate,
  53. duration: duration
  54. )
  55. }
  56. private var bolusRecommendationNotice: BolusRecommendationNotice? {
  57. switch self {
  58. case .suspend(min: let minimum):
  59. return .glucoseBelowSuspendThreshold(minGlucose: minimum)
  60. case .inRange:
  61. return .predictedGlucoseInRange
  62. case .entirelyBelowRange(min: let min, minTarget: _, units: _):
  63. return .allGlucoseBelowTarget(minGlucose: min)
  64. case .aboveRange(min: let min, correcting: _, minTarget: let target, units: let units):
  65. if units > 0 && min.quantity < target {
  66. return .predictedGlucoseBelowTarget(minGlucose: min)
  67. } else {
  68. return nil
  69. }
  70. }
  71. }
  72. /// Determines the bolus needed to perform the correction, subtracting any insulin already scheduled for
  73. /// delivery, such as the remaining portion of an ongoing temp basal.
  74. ///
  75. /// - Parameters:
  76. /// - pendingInsulin: The number of units expected to be delivered, but not yet reflected in the correction
  77. /// - maxBolus: The maximum allowable bolus value in units
  78. /// - volumeRounder: Method to round computed dose to deliverable volume
  79. /// - Returns: A bolus recommendation
  80. fileprivate func asManualBolus(
  81. pendingInsulin: Double,
  82. maxBolus: Double,
  83. volumeRounder: ((Double) -> Double)?
  84. ) -> ManualBolusRecommendation {
  85. var units = self.units - pendingInsulin
  86. units = Swift.min(maxBolus, Swift.max(0, units))
  87. units = volumeRounder?(units) ?? units
  88. return ManualBolusRecommendation(
  89. amount: units,
  90. pendingInsulin: pendingInsulin,
  91. notice: bolusRecommendationNotice
  92. )
  93. }
  94. /// Determines the bolus amount to perform a partial application correction
  95. ///
  96. /// - Parameters:
  97. /// - partialApplicationFactor: The fraction of needed insulin to deliver now
  98. /// - maxBolus: The maximum allowable bolus value in units
  99. /// - volumeRounder: Method to round computed dose to deliverable volume
  100. /// - Returns: A bolus recommendation
  101. fileprivate func asPartialBolus(
  102. partialApplicationFactor: Double,
  103. maxBolusUnits: Double,
  104. volumeRounder: ((Double) -> Double)?
  105. ) -> Double {
  106. let partialDose = units * partialApplicationFactor
  107. return Swift.min(Swift.max(0, volumeRounder?(partialDose) ?? partialDose),volumeRounder?(maxBolusUnits) ?? maxBolusUnits)
  108. }
  109. }
  110. extension TempBasalRecommendation {
  111. /// Equates the recommended rate with another rate
  112. ///
  113. /// - Parameter unitsPerHour: The rate to compare
  114. /// - Returns: Whether the rates are equal within Double precision
  115. private func matchesRate(_ unitsPerHour: Double) -> Bool {
  116. return abs(self.unitsPerHour - unitsPerHour) < .ulpOfOne
  117. }
  118. /// Determines whether the recommendation is necessary given the current state of the pump
  119. ///
  120. /// - Parameters:
  121. /// - date: The date the recommendation would be delivered
  122. /// - scheduledBasalRate: The scheduled basal rate at `date`
  123. /// - lastTempBasal: The previously set temp basal
  124. /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command
  125. /// - scheduledBasalRateMatchesPump: A flag describing whether `scheduledBasalRate` matches the scheduled basal rate of the pump.
  126. /// If `false` and the recommendation matches `scheduledBasalRate`, the temp will be recommended
  127. /// at the scheduled basal rate rather than recommending no temp.
  128. /// - Returns: A temp basal recommendation
  129. func ifNecessary(
  130. at date: Date,
  131. scheduledBasalRate: Double,
  132. lastTempBasal: DoseEntry?,
  133. continuationInterval: TimeInterval,
  134. scheduledBasalRateMatchesPump: Bool
  135. ) -> TempBasalRecommendation? {
  136. // Adjust behavior for the currently active temp basal
  137. if let lastTempBasal = lastTempBasal,
  138. lastTempBasal.type == .tempBasal,
  139. lastTempBasal.endDate > date
  140. {
  141. /// If the last temp basal has the same rate, and has more than `continuationInterval` of time remaining, don't set a new temp
  142. if matchesRate(lastTempBasal.unitsPerHour),
  143. lastTempBasal.endDate.timeIntervalSince(date) > continuationInterval {
  144. return nil
  145. } else if matchesRate(scheduledBasalRate), scheduledBasalRateMatchesPump {
  146. // If our new temp matches the scheduled rate of the pump, cancel the current temp
  147. return .cancel
  148. }
  149. } else if matchesRate(scheduledBasalRate), scheduledBasalRateMatchesPump {
  150. // If we recommend the in-progress scheduled basal rate of the pump, do nothing
  151. return nil
  152. }
  153. return self
  154. }
  155. }
  156. /// Computes a total insulin amount necessary to correct a glucose differential at a given sensitivity
  157. ///
  158. /// - Parameters:
  159. /// - fromValue: The starting glucose value
  160. /// - toValue: The desired glucose value
  161. /// - effectedSensitivity: The sensitivity, in glucose-per-insulin-unit
  162. /// - Returns: The insulin correction in units
  163. private func insulinCorrectionUnits(fromValue: Double, toValue: Double, effectedSensitivity: Double) -> Double? {
  164. guard effectedSensitivity > 0 else {
  165. return nil
  166. }
  167. let glucoseCorrection = fromValue - toValue
  168. return glucoseCorrection / effectedSensitivity
  169. }
  170. /// Computes a target glucose value for a correction, at a given time during the insulin effect duration
  171. ///
  172. /// - Parameters:
  173. /// - percentEffectDuration: The percent of time elapsed of the insulin effect duration
  174. /// - minValue: The minimum (starting) target value
  175. /// - maxValue: The maximum (eventual) target value
  176. /// - Returns: A target value somewhere between the minimum and maximum
  177. private func targetGlucoseValue(percentEffectDuration: Double, minValue: Double, maxValue: Double) -> Double {
  178. // The inflection point in time: before it we use minValue, after it we linearly blend from minValue to maxValue
  179. let useMinValueUntilPercent = 0.5
  180. guard percentEffectDuration > useMinValueUntilPercent else {
  181. return minValue
  182. }
  183. guard percentEffectDuration < 1 else {
  184. return maxValue
  185. }
  186. let slope = (maxValue - minValue) / (1 - useMinValueUntilPercent)
  187. return minValue + slope * (percentEffectDuration - useMinValueUntilPercent)
  188. }
  189. extension Collection where Element: GlucoseValue {
  190. /// For a collection of glucose prediction, determine the least amount of insulin delivered at
  191. /// `date` to correct the predicted glucose to the middle of `correctionRange` at the time of prediction.
  192. ///
  193. /// - Parameters:
  194. /// - correctionRange: The schedule of glucose values used for correction
  195. /// - date: The date the insulin correction is delivered
  196. /// - suspendThreshold: The glucose value below which only suspension is returned
  197. /// - sensitivity: The insulin sensitivity at the time of delivery
  198. /// - model: The insulin effect model
  199. /// - Returns: A correction value in units, or nil if no correction needed
  200. private func insulinCorrection(
  201. to correctionRange: GlucoseRangeSchedule,
  202. at date: Date,
  203. suspendThreshold: HKQuantity,
  204. sensitivity: HKQuantity,
  205. model: InsulinModel
  206. ) -> InsulinCorrection? {
  207. let effectDuration = model.effectDuration
  208. let timeline = [AbsoluteScheduleValue(startDate: date, endDate: date.addingTimeInterval(effectDuration), value: sensitivity)]
  209. return insulinCorrection(
  210. to: correctionRange,
  211. at: date,
  212. suspendThreshold: suspendThreshold,
  213. insulinSensitivityTimeline: timeline,
  214. model: model)
  215. }
  216. /// For a collection of glucose prediction, determine the least amount of insulin delivered at
  217. /// `date` to correct the predicted glucose to the middle of `correctionRange` at the time of prediction.
  218. ///
  219. /// - Parameters:
  220. /// - correctionRange: The schedule of glucose values used for correction
  221. /// - date: The date the insulin correction is delivered
  222. /// - suspendThreshold: The glucose value below which only suspension is returned
  223. /// - insulinSensitivityTimeline: The timeline of expected insulin sensitivity over the period of dose absorption
  224. /// - model: The insulin effect model
  225. /// - Returns: A correction value in units, or nil if no correction needed
  226. private func insulinCorrection(
  227. to correctionRange: GlucoseRangeSchedule,
  228. at date: Date,
  229. suspendThreshold: HKQuantity,
  230. insulinSensitivityTimeline: [AbsoluteScheduleValue<HKQuantity>],
  231. model: InsulinModel
  232. ) -> InsulinCorrection? {
  233. var minGlucose: GlucoseValue?
  234. var eventualGlucose: GlucoseValue?
  235. var correctingGlucose: GlucoseValue?
  236. var minCorrectionUnits: Double?
  237. var effectedSensitivityAtMinGlucose: Double?
  238. // Only consider predictions within the model's effect duration
  239. let validDateRange = DateInterval(start: date, duration: model.effectDuration)
  240. let unit = correctionRange.unit
  241. let suspendThresholdValue = suspendThreshold.doubleValue(for: unit)
  242. // For each prediction above target, determine the amount of insulin necessary to correct glucose based on the modeled effectiveness of the insulin at that time
  243. for prediction in self {
  244. guard validDateRange.contains(prediction.startDate) else {
  245. continue
  246. }
  247. // If any predicted value is below the suspend threshold, return immediately
  248. guard prediction.quantity >= suspendThreshold else {
  249. print("Suspend!")
  250. return .suspend(min: prediction)
  251. }
  252. eventualGlucose = prediction
  253. let predictedGlucoseValue = prediction.quantity.doubleValue(for: unit)
  254. let time = prediction.startDate.timeIntervalSince(date)
  255. // Compute the target value as a function of time since the dose started
  256. let targetValue = targetGlucoseValue(
  257. percentEffectDuration: time / model.effectDuration,
  258. minValue: suspendThresholdValue,
  259. maxValue: correctionRange.quantityRange(at: prediction.startDate).averageValue(for: unit)
  260. )
  261. // Compute the dose required to bring this prediction to target:
  262. // dose = (Glucose Δ) / (% effect × sensitivity)
  263. let isfSegments = insulinSensitivityTimeline.filterDateRange(date, prediction.startDate)
  264. let effectedSensitivity = isfSegments.reduce(0) { partialResult, segment in
  265. let start = Swift.max(date, segment.startDate).timeIntervalSince(date)
  266. let end = Swift.min(prediction.startDate, segment.endDate).timeIntervalSince(date)
  267. let percentEffected = model.percentEffectRemaining(at: start) - model.percentEffectRemaining(at: end)
  268. return percentEffected * segment.value.doubleValue(for: unit)
  269. }
  270. // Update range statistics
  271. if minGlucose == nil || prediction.quantity < minGlucose!.quantity {
  272. minGlucose = prediction
  273. effectedSensitivityAtMinGlucose = effectedSensitivity
  274. }
  275. guard let correctionUnits = insulinCorrectionUnits(
  276. fromValue: predictedGlucoseValue,
  277. toValue: targetValue,
  278. effectedSensitivity: Swift.max(.ulpOfOne, effectedSensitivity)
  279. ), correctionUnits > 0 else {
  280. continue
  281. }
  282. // Update the correction only if we've found a new minimum
  283. guard minCorrectionUnits == nil || correctionUnits < minCorrectionUnits! else {
  284. continue
  285. }
  286. correctingGlucose = prediction
  287. minCorrectionUnits = correctionUnits
  288. }
  289. guard let eventualGlucose, let minGlucose else {
  290. return nil
  291. }
  292. // Choose either the minimum glucose or eventual glucose as the correction delta
  293. let minGlucoseTargets = correctionRange.quantityRange(at: minGlucose.startDate)
  294. let eventualGlucoseTargets = correctionRange.quantityRange(at: eventualGlucose.startDate)
  295. // Treat the mininum glucose when both are below range
  296. if minGlucose.quantity < minGlucoseTargets.lowerBound &&
  297. eventualGlucose.quantity < eventualGlucoseTargets.lowerBound
  298. {
  299. guard let units = insulinCorrectionUnits(
  300. fromValue: minGlucose.quantity.doubleValue(for: unit),
  301. toValue: minGlucoseTargets.averageValue(for: unit),
  302. effectedSensitivity: Swift.max(.ulpOfOne, effectedSensitivityAtMinGlucose!)
  303. ) else {
  304. return nil
  305. }
  306. return .entirelyBelowRange(
  307. min: minGlucose,
  308. minTarget: minGlucoseTargets.lowerBound,
  309. units: units
  310. )
  311. } else if eventualGlucose.quantity > eventualGlucoseTargets.upperBound,
  312. let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
  313. {
  314. return .aboveRange(
  315. min: minGlucose,
  316. correcting: correctingGlucose,
  317. minTarget: eventualGlucoseTargets.lowerBound,
  318. units: minCorrectionUnits
  319. )
  320. } else {
  321. return .inRange
  322. }
  323. }
  324. /// Recommends a temporary basal rate to conform a glucose prediction timeline to a correction range
  325. ///
  326. /// Returns nil if the normal scheduled basal, or active temporary basal, is sufficient.
  327. ///
  328. /// - Parameters:
  329. /// - correctionRange: The schedule of correction ranges
  330. /// - date: The date at which the temp basal would be scheduled, defaults to now
  331. /// - suspendThreshold: A glucose value causing a recommendation of no insulin if any prediction falls below
  332. /// - sensitivity: The schedule of insulin sensitivities
  333. /// - model: The insulin absorption model
  334. /// - basalRates: The schedule of basal rates
  335. /// - additionalActiveInsulinClamp: Max amount of additional insulin above scheduled basal rate allowed to be scheduled
  336. /// - maxBasalRate: The maximum allowed basal rate
  337. /// - lastTempBasal: The previously set temp basal
  338. /// - rateRounder: Closure that rounds recommendation to nearest supported rate. If nil, no rounding is performed
  339. /// - isBasalRateScheduleOverrideActive: A flag describing whether a basal rate schedule override is in progress
  340. /// - duration: The duration of the temporary basal
  341. /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command
  342. /// - Returns: The recommended temporary basal rate and duration
  343. public func recommendedTempBasal(
  344. to correctionRange: GlucoseRangeSchedule,
  345. at date: Date = Date(),
  346. suspendThreshold: HKQuantity?,
  347. sensitivity: InsulinSensitivitySchedule,
  348. model: InsulinModel,
  349. basalRates: BasalRateSchedule,
  350. maxBasalRate: Double,
  351. additionalActiveInsulinClamp: Double? = nil,
  352. lastTempBasal: DoseEntry?,
  353. rateRounder: ((Double) -> Double)? = nil,
  354. isBasalRateScheduleOverrideActive: Bool = false,
  355. duration: TimeInterval = TimeInterval(30 * 60),
  356. continuationInterval: TimeInterval = TimeInterval(60 * 11)
  357. ) -> TempBasalRecommendation? {
  358. let correction = self.insulinCorrection(
  359. to: correctionRange,
  360. at: date,
  361. suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
  362. sensitivity: sensitivity.quantity(at: date),
  363. model: model
  364. )
  365. let scheduledBasalRate = basalRates.value(at: date)
  366. var maxBasalRate = maxBasalRate
  367. // TODO: Allow `highBasalThreshold` to be a configurable setting
  368. if case .aboveRange(min: let min, correcting: _, minTarget: let highBasalThreshold, units: _)? = correction,
  369. min.quantity < highBasalThreshold
  370. {
  371. maxBasalRate = scheduledBasalRate
  372. }
  373. if let additionalActiveInsulinClamp {
  374. let maxThirtyMinuteRateToKeepIOBBelowLimit = additionalActiveInsulinClamp * 2.0 + scheduledBasalRate // 30 minutes of a U/hr rate
  375. maxBasalRate = Swift.min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasalRate)
  376. }
  377. let temp = correction?.asTempBasal(
  378. scheduledBasalRate: scheduledBasalRate,
  379. maxBasalRate: maxBasalRate,
  380. duration: duration,
  381. rateRounder: rateRounder
  382. )
  383. return temp?.ifNecessary(
  384. at: date,
  385. scheduledBasalRate: scheduledBasalRate,
  386. lastTempBasal: lastTempBasal,
  387. continuationInterval: continuationInterval,
  388. scheduledBasalRateMatchesPump: !isBasalRateScheduleOverrideActive
  389. )
  390. }
  391. /// Recommends a dose suitable for automatic enactment. Uses boluses for high corrections, and temp basals for low corrections.
  392. ///
  393. /// Returns nil if the normal scheduled basal, or active temporary basal, is sufficient.
  394. ///
  395. /// - Parameters:
  396. /// - correctionRange: The schedule of correction ranges
  397. /// - date: The date at which the temp basal would be scheduled, defaults to now
  398. /// - suspendThreshold: A glucose value causing a recommendation of no insulin if any prediction falls below
  399. /// - sensitivity: The schedule of insulin sensitivities
  400. /// - model: The insulin absorption model
  401. /// - basalRates: The schedule of basal rates
  402. /// - maxBasalRate: The maximum allowed basal rate
  403. /// - lastTempBasal: The previously set temp basal
  404. /// - rateRounder: Closure that rounds recommendation to nearest supported rate. If nil, no rounding is performed
  405. /// - isBasalRateScheduleOverrideActive: A flag describing whether a basal rate schedule override is in progress
  406. /// - duration: The duration of the temporary basal
  407. /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command
  408. /// - Returns: The recommended dosing, or nil if no dose adjustment recommended
  409. public func recommendedAutomaticDose(
  410. to correctionRange: GlucoseRangeSchedule,
  411. at date: Date = Date(),
  412. suspendThreshold: HKQuantity?,
  413. sensitivity: InsulinSensitivitySchedule,
  414. model: InsulinModel,
  415. basalRates: BasalRateSchedule,
  416. maxAutomaticBolus: Double,
  417. partialApplicationFactor: Double,
  418. lastTempBasal: DoseEntry?,
  419. volumeRounder: ((Double) -> Double)? = nil,
  420. rateRounder: ((Double) -> Double)? = nil,
  421. isBasalRateScheduleOverrideActive: Bool = false,
  422. duration: TimeInterval = TimeInterval(30 * 60),
  423. continuationInterval: TimeInterval = TimeInterval(11 * 60)
  424. ) -> AutomaticDoseRecommendation? {
  425. guard let correction = self.insulinCorrection(
  426. to: correctionRange,
  427. at: date,
  428. suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
  429. sensitivity: sensitivity.quantity(at: date),
  430. model: model
  431. ) else {
  432. return nil
  433. }
  434. let scheduledBasalRate = basalRates.value(at: date)
  435. var maxAutomaticBolus = maxAutomaticBolus
  436. if case .aboveRange(min: let min, correcting: _, minTarget: let doseThreshold, units: _) = correction,
  437. min.quantity < doseThreshold
  438. {
  439. maxAutomaticBolus = 0
  440. }
  441. var temp: TempBasalRecommendation? = correction.asTempBasal(
  442. scheduledBasalRate: scheduledBasalRate,
  443. maxBasalRate: scheduledBasalRate,
  444. duration: duration,
  445. rateRounder: rateRounder
  446. )
  447. temp = temp?.ifNecessary(
  448. at: date,
  449. scheduledBasalRate: scheduledBasalRate,
  450. lastTempBasal: lastTempBasal,
  451. continuationInterval: continuationInterval,
  452. scheduledBasalRateMatchesPump: !isBasalRateScheduleOverrideActive
  453. )
  454. let bolusUnits = correction.asPartialBolus(
  455. partialApplicationFactor: partialApplicationFactor,
  456. maxBolusUnits: maxAutomaticBolus,
  457. volumeRounder: volumeRounder
  458. )
  459. if temp != nil || bolusUnits > 0 {
  460. return AutomaticDoseRecommendation(basalAdjustment: temp, bolusUnits: bolusUnits)
  461. }
  462. return nil
  463. }
  464. /// Recommends a bolus to conform a glucose prediction timeline to a correction range
  465. ///
  466. /// - Parameters:
  467. /// - correctionRange: The schedule of correction ranges
  468. /// - date: The date at which the bolus would apply, defaults to now
  469. /// - suspendThreshold: A glucose value causing a recommendation of no insulin if any prediction falls below
  470. /// - sensitivity: The schedule of insulin sensitivities
  471. /// - model: The insulin absorption model
  472. /// - pendingInsulin: The number of units expected to be delivered, but not yet reflected in the correction
  473. /// - maxBolus: The maximum bolus to return
  474. /// - volumeRounder: Closure that rounds recommendation to nearest supported bolus volume. If nil, no rounding is performed
  475. /// - Returns: A bolus recommendation
  476. public func recommendedManualBolus(
  477. to correctionRange: GlucoseRangeSchedule,
  478. at date: Date = Date(),
  479. suspendThreshold: HKQuantity?,
  480. sensitivity: InsulinSensitivitySchedule,
  481. model: InsulinModel,
  482. pendingInsulin: Double,
  483. maxBolus: Double,
  484. volumeRounder: ((Double) -> Double)? = nil
  485. ) -> ManualBolusRecommendation {
  486. guard let correction = self.insulinCorrection(
  487. to: correctionRange,
  488. at: date,
  489. suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
  490. sensitivity: sensitivity.quantity(at: date),
  491. model: model
  492. ) else {
  493. return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin)
  494. }
  495. var bolus = correction.asManualBolus(
  496. pendingInsulin: pendingInsulin,
  497. maxBolus: maxBolus,
  498. volumeRounder: volumeRounder
  499. )
  500. // Handle the "current BG below target" notice here
  501. // TODO: Don't assume in the future that the first item in the array is current BG
  502. if case .predictedGlucoseBelowTarget? = bolus.notice,
  503. let first = first, first.quantity < correctionRange.quantityRange(at: first.startDate).lowerBound
  504. {
  505. bolus.notice = .currentGlucoseBelowTarget(glucose: first)
  506. }
  507. return bolus
  508. }
  509. }