DosingEngine.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. import Foundation
  2. enum DosingEngine {
  3. struct DosingInputs {
  4. let reason: String
  5. let carbsRequired: (carbs: Decimal, minutes: Decimal)?
  6. }
  7. /// struct to keep the relevant state needed for the output of the SMB decision logic
  8. struct SMBDecision {
  9. let isEnabled: Bool
  10. let minGuardGlucose: Decimal?
  11. let reason: String?
  12. }
  13. /// checks to see if SMB are enabled via the profile
  14. private static func isProfileSmbEnabled(
  15. currentGlucose: Decimal,
  16. adjustedTargetGlucose: Decimal,
  17. profile: Profile,
  18. meal: ComputedCarbs,
  19. trioCustomOrefVariables: TrioCustomOrefVariables,
  20. clock: Date
  21. ) throws -> Bool {
  22. if trioCustomOrefVariables.smbIsOff {
  23. return false
  24. }
  25. if try isSmbScheduledOff(trioCustomOrefVariables: trioCustomOrefVariables, clock: clock) {
  26. return false
  27. }
  28. if trioCustomOrefVariables.shouldProtectDueToHIGH {
  29. return false
  30. }
  31. if !profile.allowSMBWithHighTemptarget, profile.temptargetSet == true, adjustedTargetGlucose > 100 {
  32. return false
  33. }
  34. if profile.enableSMBAlways {
  35. return true
  36. }
  37. if profile.enableSMBWithCOB, meal.mealCOB > 0 {
  38. return true
  39. }
  40. if profile.enableSMBAfterCarbs, meal.carbs > 0 {
  41. return true
  42. }
  43. if profile.enableSMBWithTemptarget, profile.temptargetSet == true, adjustedTargetGlucose < 100 {
  44. return true
  45. }
  46. if profile.enableSMBHighBg, currentGlucose >= profile.enableSMBHighBgTarget {
  47. return true
  48. }
  49. return false
  50. }
  51. /// helper function to check if SMB is scheduled off given the current timezone
  52. private static func isSmbScheduledOff(trioCustomOrefVariables: TrioCustomOrefVariables, clock: Date) throws -> Bool {
  53. guard trioCustomOrefVariables.smbIsScheduledOff else {
  54. return false
  55. }
  56. guard let currentHour = clock.hourInLocalTime.map({ Decimal($0) }) else {
  57. throw CalendarError.invalidCalendarHourOnly
  58. }
  59. let startHour = trioCustomOrefVariables.start
  60. let endHour = trioCustomOrefVariables.end
  61. // SMBs will be disabled from [start, end) local time
  62. if startHour < endHour, currentHour >= startHour && currentHour < endHour {
  63. // disable when the schedule does not wrap around midnight
  64. return true
  65. } else if startHour > endHour, currentHour >= startHour || currentHour < endHour {
  66. // disable when the schedule does wrap around midnight
  67. return true
  68. } else if startHour == 0, endHour == 0 {
  69. // schedule specifies the entire day
  70. return true
  71. } else if startHour == endHour, currentHour == startHour {
  72. // one hour of scheduled off SMB
  73. return true
  74. }
  75. return false
  76. }
  77. /// helper function for reason string glucose output
  78. static func convertGlucose(profile: Profile, glucose: Decimal) -> Decimal {
  79. let units = profile.outUnits ?? .mgdL
  80. switch units {
  81. case .mgdL: return glucose.jsRounded()
  82. case .mmolL: return glucose.asMmolL
  83. }
  84. }
  85. /// Top level smb enabling logic
  86. ///
  87. /// This function includes both the profile / customOrefVariable checks from JS `enable_smb` as
  88. /// well as some of the later checks from `determineBasal` that can disable SMB
  89. static func makeSMBDosingDecision(
  90. profile: Profile,
  91. meal: ComputedCarbs,
  92. currentGlucose: Decimal,
  93. adjustedTargetGlucose: Decimal,
  94. minGuardGlucose: Decimal,
  95. threshold: Decimal,
  96. glucoseStatus: GlucoseStatus,
  97. trioCustomOrefVariables: TrioCustomOrefVariables,
  98. clock: Date
  99. ) throws -> SMBDecision {
  100. var smbIsEnabled = try isProfileSmbEnabled(
  101. currentGlucose: currentGlucose,
  102. adjustedTargetGlucose: adjustedTargetGlucose,
  103. profile: profile,
  104. meal: meal,
  105. trioCustomOrefVariables: trioCustomOrefVariables,
  106. clock: clock
  107. )
  108. // these last two checks are implemented outside of the core enable_smb
  109. // function in JS but we should keep all of the smb enabling logic
  110. // in one place. Note: We can't shortcut the return value because
  111. // the determineBasal logic always evaluates this logic
  112. var minGuardGlucoseDecision: Decimal?
  113. var reason: String?
  114. if smbIsEnabled, minGuardGlucose < threshold {
  115. minGuardGlucoseDecision = minGuardGlucose
  116. smbIsEnabled = false
  117. }
  118. let maxDeltaGlucoseThreshold = min(profile.maxDeltaBgThreshold, 0.4)
  119. if glucoseStatus.maxDelta > maxDeltaGlucoseThreshold * currentGlucose {
  120. reason =
  121. "maxDelta \(convertGlucose(profile: profile, glucose: glucoseStatus.maxDelta)) > \(100 * maxDeltaGlucoseThreshold)% of BG \(convertGlucose(profile: profile, glucose: currentGlucose)) - SMB disabled!, "
  122. smbIsEnabled = false
  123. }
  124. return SMBDecision(
  125. isEnabled: smbIsEnabled,
  126. minGuardGlucose: minGuardGlucoseDecision,
  127. reason: reason
  128. )
  129. }
  130. static func prepareDosingInputs(
  131. profile: Profile,
  132. mealData: ComputedCarbs,
  133. forecast: ForecastResult,
  134. naiveEventualGlucose: Decimal,
  135. threshold: Decimal,
  136. glucoseImpact: Decimal,
  137. deviation: Decimal,
  138. currentBasal: Decimal,
  139. overrideFactor: Decimal,
  140. adjustedSensitivity: Decimal,
  141. isfReason: String,
  142. tddReason: String,
  143. targetLog: String // This is a pre-formatted string from the JS
  144. ) -> DosingInputs {
  145. let lastIOBpredBG = forecast.iob.last ?? 0
  146. let lastCOBpredBG = forecast.cob?.last
  147. let lastUAMpredBG = forecast.uam?.last
  148. var reason =
  149. "\(isfReason), COB: \(mealData.mealCOB), Dev: \(deviation), BGI: \(glucoseImpact), CR: \(forecast.adjustedCarbRatio), Target: \(targetLog), minPredBG \(forecast.minForecastedGlucose), minGuardBG \(forecast.minGuardGlucose), IOBpredBG \(lastIOBpredBG)"
  150. if let lastCOB = lastCOBpredBG {
  151. reason += ", COBpredBG \(lastCOB)"
  152. }
  153. if let lastUAM = lastUAMpredBG {
  154. reason += ", UAMpredBG \(lastUAM)"
  155. }
  156. reason += tddReason
  157. reason += "; " // Start of conclusion
  158. let carbsRequiredResult = calculateCarbsRequired(
  159. profile: profile,
  160. mealData: mealData,
  161. naiveEventualGlucose: naiveEventualGlucose,
  162. minGuardGlucose: forecast.minGuardGlucose,
  163. threshold: threshold,
  164. iobForecast: forecast.iob,
  165. cobForecast: forecast.internalCob,
  166. carbImpact: forecast.carbImpact,
  167. remainingCarbImpactPeak: forecast.remainingCarbImpactPeak,
  168. currentBasal: currentBasal,
  169. overrideFactor: overrideFactor,
  170. adjustedSensitivity: adjustedSensitivity,
  171. adjustedCarbRatio: forecast.adjustedCarbRatio
  172. )
  173. if let result = carbsRequiredResult {
  174. reason += "\(result.carbs) add'l carbs req w/in \(result.minutes)m; "
  175. }
  176. return DosingInputs(reason: reason, carbsRequired: carbsRequiredResult)
  177. }
  178. /// Calculates the carbohydrates required to avoid a potential hypoglycemic event.
  179. ///
  180. /// - Returns: A tuple containing the required carbs and minutes until BG is below threshold, or `nil` if no carbs are required.
  181. static func calculateCarbsRequired(
  182. profile: Profile,
  183. mealData: ComputedCarbs,
  184. naiveEventualGlucose: Decimal,
  185. minGuardGlucose: Decimal,
  186. threshold: Decimal,
  187. iobForecast: [Decimal],
  188. cobForecast: [Decimal],
  189. carbImpact: Decimal,
  190. remainingCarbImpactPeak: Decimal,
  191. currentBasal: Decimal,
  192. overrideFactor: Decimal,
  193. adjustedSensitivity: Decimal,
  194. adjustedCarbRatio: Decimal
  195. ) -> (carbs: Decimal, minutes: Decimal)? {
  196. var carbsRequiredGlucose = naiveEventualGlucose
  197. if naiveEventualGlucose < 40 {
  198. carbsRequiredGlucose = min(minGuardGlucose, naiveEventualGlucose)
  199. }
  200. let glucoseUndershoot = threshold - carbsRequiredGlucose
  201. var minutesAboveThreshold = Decimal(240)
  202. let useCOBForecast = mealData.mealCOB > 0 && (carbImpact > 0 || remainingCarbImpactPeak > 0)
  203. let forecast = useCOBForecast ? cobForecast : iobForecast
  204. // At this point in the JS the forecasts have already been rounded
  205. for (index, glucose) in forecast.map({ $0.jsRounded() }).enumerated() {
  206. if glucose < threshold {
  207. minutesAboveThreshold = Decimal(5) * Decimal(index)
  208. break
  209. }
  210. }
  211. let zeroTempDuration = minutesAboveThreshold
  212. let zeroTempEffect = currentBasal * adjustedSensitivity * overrideFactor * zeroTempDuration / 60
  213. let mealCarbs = mealData.carbs
  214. let cobForCarbsRequired = max(0, mealData.mealCOB - (Decimal(0.25) * mealCarbs))
  215. guard adjustedCarbRatio > 0 else { return nil }
  216. let carbSensitivityFactor = adjustedSensitivity / adjustedCarbRatio
  217. guard carbSensitivityFactor > 0 else { return nil }
  218. var carbsRequired = (glucoseUndershoot - zeroTempEffect) / carbSensitivityFactor - cobForCarbsRequired
  219. carbsRequired = carbsRequired.rounded(toPlaces: 0)
  220. let carbsRequiredThreshold = profile.carbsReqThreshold
  221. if carbsRequired >= carbsRequiredThreshold, minutesAboveThreshold <= 45 {
  222. return (carbs: carbsRequired, minutes: minutesAboveThreshold)
  223. }
  224. return nil
  225. }
  226. /// Determines if a low glucose suspend is warranted.
  227. ///
  228. /// This function checks for low glucose conditions and may modify the determination object
  229. /// with a suspend recommendation and an updated reason string.
  230. ///
  231. /// - Returns: A tuple containing:
  232. /// - `setTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
  233. /// - `determination`: The (potentially modified) determination object.
  234. static func lowGlucoseSuspend(
  235. currentGlucose: Decimal,
  236. minGuardGlucose: Decimal,
  237. iob: Decimal,
  238. minDelta: Decimal,
  239. expectedDelta: Decimal,
  240. threshold: Decimal,
  241. overrideFactor: Decimal,
  242. profile: Profile,
  243. adjustedSensitivity: Decimal,
  244. targetGlucose: Decimal,
  245. currentTemp: TempBasal,
  246. determination: Determination
  247. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  248. var newDetermination = determination
  249. guard let currentBasal = profile.currentBasal else {
  250. // Should have been checked earlier
  251. throw TempBasalFunctionError.invalidBasalRateOnProfile
  252. }
  253. let suspendThreshold = -currentBasal * overrideFactor * 20 / 60
  254. if currentGlucose < threshold, iob < suspendThreshold, minDelta > 0, minDelta > expectedDelta {
  255. let iobString = String(describing: iob)
  256. let suspendString = String(describing: suspendThreshold.jsRounded(scale: 2))
  257. let minDeltaString = String(describing: convertGlucose(profile: profile, glucose: minDelta))
  258. let expectedDeltaString = String(describing: convertGlucose(profile: profile, glucose: expectedDelta))
  259. newDetermination
  260. .reason +=
  261. "IOB \(iobString) < \(suspendString) and minDelta \(minDeltaString) > expectedDelta \(expectedDeltaString); "
  262. return (shouldSetTempBasal: false, determination: newDetermination)
  263. } else if currentGlucose < threshold || minGuardGlucose < threshold {
  264. let minGuardGlucoseString = String(describing: convertGlucose(profile: profile, glucose: minGuardGlucose))
  265. let thresholdString = String(describing: convertGlucose(profile: profile, glucose: threshold))
  266. newDetermination.reason += "minGuardBG \(minGuardGlucoseString) < \(thresholdString)"
  267. let glucoseUndershoot = targetGlucose - minGuardGlucose
  268. if minGuardGlucose < threshold {
  269. newDetermination.minGuardBG = minGuardGlucose
  270. }
  271. let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
  272. var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
  273. durationRequired = (durationRequired / 30).jsRounded() * 30
  274. durationRequired = max(30, min(120, durationRequired))
  275. let finalDetermination = try TempBasalFunctions.setTempBasal(
  276. rate: 0,
  277. duration: durationRequired,
  278. profile: profile,
  279. determination: newDetermination,
  280. currentTemp: currentTemp
  281. )
  282. return (shouldSetTempBasal: true, determination: finalDetermination)
  283. }
  284. return (shouldSetTempBasal: false, determination: determination)
  285. }
  286. /// Determines if a neutral temp basal should be skipped to avoid pump alerts.
  287. ///
  288. /// - Returns: A tuple containing:
  289. /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
  290. /// - `determination`: The (potentially modified) determination object.
  291. static func skipNeutralTempBasal(
  292. smbIsEnabled: Bool,
  293. profile: Profile,
  294. clock: Date,
  295. currentTemp: TempBasal,
  296. determination: Determination
  297. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  298. guard profile.skipNeutralTemps else {
  299. return (shouldSetTempBasal: false, determination: determination)
  300. }
  301. guard let totalMinutes = clock.minutesSinceMidnight else {
  302. throw CalendarError.invalidCalendar
  303. }
  304. let minute = totalMinutes % 60
  305. guard minute >= 55 else {
  306. return (shouldSetTempBasal: false, determination: determination)
  307. }
  308. if !smbIsEnabled {
  309. var newDetermination = determination
  310. let minutesLeft = 60 - minute
  311. newDetermination
  312. .reason +=
  313. "; Canceling temp at \(minutesLeft)min before turn of the hour to avoid beeping of MDT. SMB are disabled anyways."
  314. let finalDetermination = try TempBasalFunctions.setTempBasal(
  315. rate: 0,
  316. duration: 0,
  317. profile: profile,
  318. determination: newDetermination,
  319. currentTemp: currentTemp
  320. )
  321. return (shouldSetTempBasal: true, determination: finalDetermination)
  322. } else {
  323. // In the JS, this path logs to the console but does not modify determination.
  324. // We will do nothing here to match that behavior.
  325. return (shouldSetTempBasal: false, determination: determination)
  326. }
  327. }
  328. /// Handles the case where eventual glucose is predicted to be low.
  329. ///
  330. /// - Returns: A tuple containing:
  331. /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
  332. /// - `determination`: The (potentially modified) determination object.
  333. static func handleLowEventualGlucose(
  334. eventualGlucose: Decimal,
  335. minGlucose: Decimal,
  336. targetGlucose: Decimal,
  337. minDelta: Decimal,
  338. expectedDelta: Decimal,
  339. carbsRequired: Decimal,
  340. naiveEventualGlucose: Decimal,
  341. glucoseStatus: GlucoseStatus,
  342. currentTemp: TempBasal,
  343. basal: Decimal,
  344. profile: Profile,
  345. determination: Determination,
  346. adjustedSensitivity: Decimal,
  347. overrideFactor: Decimal
  348. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  349. guard eventualGlucose < minGlucose else {
  350. return (shouldSetTempBasal: false, determination: determination)
  351. }
  352. var newDetermination = determination
  353. newDetermination
  354. .reason +=
  355. "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) < \(convertGlucose(profile: profile, glucose: minGlucose))"
  356. // if 5m or 30m avg BG is rising faster than expected delta
  357. if minDelta > expectedDelta, minDelta > 0, carbsRequired == 0 {
  358. if naiveEventualGlucose < 40 {
  359. newDetermination.reason += ", naive_eventualBG < 40. "
  360. let finalDetermination = try TempBasalFunctions.setTempBasal(
  361. rate: 0,
  362. duration: 30,
  363. profile: profile,
  364. determination: newDetermination,
  365. currentTemp: currentTemp
  366. )
  367. return (shouldSetTempBasal: true, determination: finalDetermination)
  368. }
  369. if glucoseStatus.delta > minDelta {
  370. newDetermination
  371. .reason +=
  372. ", but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) > expectedDelta \(convertGlucose(profile: profile, glucose: expectedDelta))"
  373. } else {
  374. newDetermination
  375. .reason +=
  376. ", but Min. Delta \(minDelta.jsRounded(scale: 2)) > Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
  377. }
  378. let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
  379. let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
  380. if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
  381. newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
  382. return (shouldSetTempBasal: true, determination: newDetermination)
  383. } else {
  384. newDetermination.reason += "; setting current basal of \(basal) as temp. "
  385. let finalDetermination = try TempBasalFunctions.setTempBasal(
  386. rate: basal,
  387. duration: 30,
  388. profile: profile,
  389. determination: newDetermination,
  390. currentTemp: currentTemp
  391. )
  392. return (shouldSetTempBasal: true, determination: finalDetermination)
  393. }
  394. }
  395. // calculate 30m low-temp required to get projected BG up to target
  396. var insulinRequired = 2 * min(0, (eventualGlucose - targetGlucose) / adjustedSensitivity)
  397. insulinRequired = insulinRequired.jsRounded(scale: 2)
  398. let naiveInsulinRequired = min(0, (naiveEventualGlucose - targetGlucose) / adjustedSensitivity).jsRounded(scale: 2)
  399. if minDelta < 0, minDelta > expectedDelta {
  400. let newInsulinRequired = (insulinRequired * (minDelta / expectedDelta)).jsRounded(scale: 2)
  401. insulinRequired = newInsulinRequired
  402. }
  403. var rate = basal + (2 * insulinRequired)
  404. rate = TempBasalFunctions.roundBasal(profile: profile, basalRate: rate)
  405. let insulinScheduled = Decimal(currentTemp.duration) * (currentTemp.rate - basal) / 60
  406. let minInsulinRequired = min(insulinRequired, naiveInsulinRequired)
  407. if insulinScheduled < minInsulinRequired - basal * 0.3 {
  408. newDetermination
  409. .reason += ", \(currentTemp.duration)m@\(currentTemp.rate.jsRounded(scale: 2)) is a lot less than needed. "
  410. let finalDetermination = try TempBasalFunctions.setTempBasal(
  411. rate: rate,
  412. duration: 30,
  413. profile: profile,
  414. determination: newDetermination,
  415. currentTemp: currentTemp
  416. )
  417. return (shouldSetTempBasal: true, determination: finalDetermination)
  418. }
  419. if currentTemp.duration > 5, rate >= currentTemp.rate * 0.8 {
  420. newDetermination.reason += ", temp \(currentTemp.rate) ~< req \(rate)U/hr. "
  421. return (shouldSetTempBasal: true, determination: newDetermination)
  422. } else {
  423. if rate <= 0 {
  424. guard let currentBasal = profile.currentBasal else {
  425. throw TempBasalFunctionError.invalidBasalRateOnProfile
  426. }
  427. let glucoseUndershoot = targetGlucose - naiveEventualGlucose
  428. let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
  429. var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
  430. if durationRequired < 0 {
  431. durationRequired = 0
  432. } else {
  433. durationRequired = (durationRequired / 30).jsRounded() * 30
  434. durationRequired = min(120, max(0, durationRequired))
  435. }
  436. if durationRequired > 0 {
  437. newDetermination.reason += ", setting \(durationRequired)m zero temp. "
  438. let finalDetermination = try TempBasalFunctions.setTempBasal(
  439. rate: rate,
  440. duration: durationRequired,
  441. profile: profile,
  442. determination: newDetermination,
  443. currentTemp: currentTemp
  444. )
  445. return (shouldSetTempBasal: true, determination: finalDetermination)
  446. }
  447. } else {
  448. newDetermination.reason += ", setting \(rate)U/hr. "
  449. }
  450. let finalDetermination = try TempBasalFunctions.setTempBasal(
  451. rate: rate,
  452. duration: 30,
  453. profile: profile,
  454. determination: newDetermination,
  455. currentTemp: currentTemp
  456. )
  457. return (shouldSetTempBasal: true, determination: finalDetermination)
  458. }
  459. }
  460. /// Handles the case where glucose is falling faster than expected.
  461. ///
  462. /// - Returns: A tuple containing:
  463. /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
  464. /// - `determination`: The (potentially modified) determination object.
  465. static func glucoseFallingFasterThanExpected(
  466. eventualGlucose: Decimal,
  467. minGlucose: Decimal,
  468. minDelta: Decimal,
  469. expectedDelta: Decimal,
  470. glucoseStatus: GlucoseStatus,
  471. currentTemp: TempBasal,
  472. basal: Decimal,
  473. smbIsEnabled: Bool,
  474. profile: Profile,
  475. determination: Determination
  476. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  477. guard minDelta < expectedDelta else {
  478. return (shouldSetTempBasal: false, determination: determination)
  479. }
  480. var newDetermination = determination
  481. if !smbIsEnabled {
  482. if glucoseStatus.delta < minDelta {
  483. newDetermination
  484. .reason +=
  485. "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
  486. } else {
  487. newDetermination
  488. .reason +=
  489. "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Min. Delta \(minDelta.jsRounded(scale: 2)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
  490. }
  491. let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
  492. let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
  493. if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
  494. newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
  495. return (shouldSetTempBasal: true, determination: newDetermination)
  496. } else {
  497. newDetermination.reason += "; setting current basal of \(basal) as temp. "
  498. let finalDetermination = try TempBasalFunctions.setTempBasal(
  499. rate: basal,
  500. duration: 30,
  501. profile: profile,
  502. determination: newDetermination,
  503. currentTemp: currentTemp
  504. )
  505. return (shouldSetTempBasal: true, determination: finalDetermination)
  506. }
  507. }
  508. return (shouldSetTempBasal: false, determination: determination)
  509. }
  510. /// Handles the case where the eventual or forecasted glucose is less than the max glucose.
  511. ///
  512. /// - Returns: A tuple containing:
  513. /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
  514. /// - `determination`: The (potentially modified) determination object.
  515. static func eventualOrForecastGlucoseLessThanMax(
  516. eventualGlucose: Decimal,
  517. maxGlucose: Decimal,
  518. minForecastGlucose: Decimal,
  519. currentTemp: TempBasal,
  520. basal: Decimal,
  521. smbIsEnabled: Bool,
  522. profile: Profile,
  523. determination: Determination
  524. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  525. guard min(eventualGlucose, minForecastGlucose) < maxGlucose else {
  526. return (shouldSetTempBasal: false, determination: determination)
  527. }
  528. var newDetermination = determination
  529. newDetermination.minPredBG = minForecastGlucose
  530. if !smbIsEnabled {
  531. newDetermination
  532. .reason +=
  533. "\(convertGlucose(profile: profile, glucose: eventualGlucose))-\(convertGlucose(profile: profile, glucose: minForecastGlucose)) in range: no temp required"
  534. let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
  535. let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
  536. if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
  537. newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
  538. return (shouldSetTempBasal: true, determination: newDetermination)
  539. } else {
  540. newDetermination.reason += "; setting current basal of \(basal) as temp. "
  541. let finalDetermination = try TempBasalFunctions.setTempBasal(
  542. rate: basal,
  543. duration: 30,
  544. profile: profile,
  545. determination: newDetermination,
  546. currentTemp: currentTemp
  547. )
  548. return (shouldSetTempBasal: true, determination: finalDetermination)
  549. }
  550. }
  551. return (shouldSetTempBasal: false, determination: determination)
  552. }
  553. /// Handles the case where IOB is greater than the max IOB.
  554. ///
  555. /// - Returns: A tuple containing:
  556. /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
  557. /// - `determination`: The (potentially modified) determination object.
  558. static func iobGreaterThanMax(
  559. iob: Decimal,
  560. maxIob: Decimal,
  561. currentTemp: TempBasal,
  562. basal: Decimal,
  563. profile: Profile,
  564. determination: Determination
  565. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  566. guard iob > maxIob else {
  567. return (shouldSetTempBasal: false, determination: determination)
  568. }
  569. var newDetermination = determination
  570. newDetermination.reason += "IOB \(iob.jsRounded(scale: 2)) > max_iob \(maxIob)"
  571. let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
  572. let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
  573. if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
  574. newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
  575. return (shouldSetTempBasal: true, determination: newDetermination)
  576. } else {
  577. newDetermination.reason += "; setting current basal of \(basal) as temp. "
  578. let finalDetermination = try TempBasalFunctions.setTempBasal(
  579. rate: basal,
  580. duration: 30,
  581. profile: profile,
  582. determination: newDetermination,
  583. currentTemp: currentTemp
  584. )
  585. return (shouldSetTempBasal: true, determination: finalDetermination)
  586. }
  587. }
  588. }