DetermineBasalGenerator.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. import Foundation
  2. protocol OverrideHandler {
  3. func overrideProfileParameters(profile: Profile, override: Override?) throws -> Profile
  4. // TODO: handle mutation of profile parameters that the user can alter using Overrides
  5. /// This could also possibly be handled via an extension of our existing `ProfileGenerator` (?)
  6. }
  7. enum DeterminationGenerator {
  8. // override data can just be fetched from the DB
  9. // handling via overrideManager ?
  10. /// Top-level determination generator, callers should use this function
  11. static func generate(
  12. profile: Profile,
  13. preferences: Preferences,
  14. currentTemp: TempBasal,
  15. iobData: [IobResult],
  16. mealData: ComputedCarbs,
  17. autosensData: Autosens,
  18. reservoirData: Decimal,
  19. glucose: [BloodGlucose],
  20. microBolusAllowed: Bool,
  21. trioCustomOrefVariables: TrioCustomOrefVariables,
  22. currentTime: Date
  23. ) throws -> Determination? {
  24. let glucoseStatus = try Self.getGlucoseStatus(glucoseReadings: glucose)
  25. guard let glucoseStatus = glucoseStatus else { throw DeterminationError.missingInputs }
  26. return try determineBasal(
  27. profile: profile,
  28. preferences: preferences,
  29. currentTemp: currentTemp,
  30. iobData: iobData,
  31. mealData: mealData,
  32. autosensData: autosensData,
  33. reservoirData: reservoirData,
  34. glucoseStatus: glucoseStatus,
  35. microBolusAllowed: microBolusAllowed,
  36. trioCustomOrefVariables: trioCustomOrefVariables,
  37. currentTime: currentTime
  38. )
  39. }
  40. /// Internal function to implement the core determine basal logic. We have a separate function
  41. /// from `generate` so that we can pass GlucoseStatus values directly into the function
  42. /// for testing.
  43. static func determineBasal(
  44. profile: Profile,
  45. preferences: Preferences,
  46. currentTemp: TempBasal,
  47. iobData: [IobResult],
  48. mealData: ComputedCarbs,
  49. autosensData: Autosens,
  50. reservoirData _: Decimal,
  51. glucoseStatus: GlucoseStatus,
  52. microBolusAllowed: Bool,
  53. trioCustomOrefVariables: TrioCustomOrefVariables,
  54. currentTime: Date
  55. ) throws -> Determination? {
  56. var autosensData = autosensData
  57. try checkDeterminationInputs(
  58. glucoseStatus: glucoseStatus,
  59. currentTemp: currentTemp,
  60. iobData: iobData,
  61. profile: profile,
  62. trioCustomOrefVariables: trioCustomOrefVariables,
  63. currentTime: currentTime,
  64. )
  65. let currentGlucose: Decimal = glucoseStatus.glucose
  66. if let errorDetermination = try handleTempBasalCases(
  67. glucoseStatus: glucoseStatus,
  68. profile: profile,
  69. currentTemp: currentTemp,
  70. currentTime: currentTime,
  71. trioCustomOrefVariables: trioCustomOrefVariables
  72. ) {
  73. return errorDetermination
  74. }
  75. // Safety check: current temp vs. last temp in iob
  76. guard let lastTempTarget = iobData.first?.lastTemp else {
  77. throw DeterminationError.missingIob
  78. }
  79. if !checkCurrentTempBasalRateSafety(
  80. currentTemp: currentTemp,
  81. lastTempTarget: lastTempTarget,
  82. currentTime: currentTime
  83. ) {
  84. let reason =
  85. "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
  86. return Determination(
  87. id: UUID(),
  88. reason: reason,
  89. units: nil,
  90. insulinReq: nil,
  91. eventualBG: nil,
  92. sensitivityRatio: nil,
  93. rate: 0,
  94. duration: 0,
  95. iob: iobData.first?.iob,
  96. cob: nil,
  97. predictions: nil,
  98. deliverAt: currentTime,
  99. carbsReq: nil,
  100. temp: .absolute,
  101. bg: glucoseStatus.glucose,
  102. reservoir: nil,
  103. isf: profile.sens,
  104. timestamp: currentTime,
  105. tdd: nil,
  106. current_target: profile.targetBg,
  107. minDelta: nil,
  108. expectedDelta: nil,
  109. minGuardBG: nil,
  110. minPredBG: nil,
  111. threshold: nil,
  112. carbRatio: profile.carbRatio,
  113. received: false
  114. )
  115. }
  116. let dynamicIsfResult = DynamicISF.calculate(
  117. profile: profile,
  118. preferences: preferences,
  119. currentGlucose: currentGlucose,
  120. trioCustomOrefVariables: trioCustomOrefVariables
  121. )
  122. if let dynamicIsfResult = dynamicIsfResult {
  123. autosensData = Autosens(
  124. ratio: dynamicIsfResult.ratio,
  125. newisf: autosensData.newisf,
  126. deviationsUnsorted: autosensData.deviationsUnsorted,
  127. timestamp: autosensData.timestamp
  128. )
  129. }
  130. let (sensitivityRatio, updateAutosensRatio) = calculateSensitivityRatio(
  131. currentGlucose: currentGlucose,
  132. profile: profile,
  133. autosens: autosensData,
  134. targetGlucose: profile.profileTarget(trioCustomOrefVariables: trioCustomOrefVariables) ?? 120,
  135. temptargetSet: profile.temptargetSet ?? false,
  136. dynamicIsfResult: dynamicIsfResult
  137. )
  138. if updateAutosensRatio {
  139. autosensData = Autosens(
  140. ratio: sensitivityRatio,
  141. newisf: autosensData.newisf,
  142. deviationsUnsorted: autosensData.deviationsUnsorted,
  143. timestamp: autosensData.timestamp
  144. )
  145. }
  146. let basal: Decimal
  147. if let dynamicIsfResult = dynamicIsfResult, profile.tddAdjBasal {
  148. basal = computeAdjustedBasal(
  149. profile: profile,
  150. currentBasalRate: profile.currentBasal ?? profile.basalFor(time: currentTime),
  151. sensitivityRatio: dynamicIsfResult.tddRatio,
  152. overrideFactor: trioCustomOrefVariables.overrideFactor()
  153. )
  154. } else {
  155. basal = computeAdjustedBasal(
  156. profile: profile,
  157. currentBasalRate: profile.currentBasal ?? profile.basalFor(time: currentTime),
  158. sensitivityRatio: sensitivityRatio,
  159. overrideFactor: trioCustomOrefVariables.overrideFactor()
  160. )
  161. }
  162. // this is the `sens` variable in JS, it's the adjusted sensitivity
  163. let adjustedSensitivity = computeAdjustedSensitivity(
  164. sensitivity: profile.sens ?? profile.sensitivityFor(time: currentTime),
  165. sensitivityRatio: sensitivityRatio,
  166. trioCustomOrefVariables: trioCustomOrefVariables
  167. )
  168. let (adjustedGlucoseTargets, threshold) = adjustGlucoseTargets(
  169. profile: profile,
  170. autosens: autosensData,
  171. trioCustomOrefVariables: trioCustomOrefVariables,
  172. temptargetSet: profile.temptargetSet ?? false,
  173. targetGlucose: profile.minBg ?? 100,
  174. minGlucose: profile.minBg ?? 70, // TODO: can we force unwrap?
  175. maxGlucose: profile.maxBg ?? 180,
  176. noise: 1
  177. )
  178. let glucoseImpactSeries = buildGlucoseImpactSeries(iobDataSeries: iobData, sensitivity: adjustedSensitivity)
  179. let glucoseImpactSeriesWithZeroTemp = buildGlucoseImpactSeries(
  180. iobDataSeries: iobData,
  181. sensitivity: adjustedSensitivity,
  182. withZeroTemp: true
  183. )
  184. guard let currentGlucoseImpact = glucoseImpactSeries.first?.jsRounded(scale: 2) else {
  185. throw DeterminationError.determinationError
  186. }
  187. let minDelta = min(glucoseStatus.delta, glucoseStatus.shortAvgDelta)
  188. let minAvgDelta = min(glucoseStatus.shortAvgDelta, glucoseStatus.longAvgDelta)
  189. let longAvgDelta = glucoseStatus.longAvgDelta
  190. let intervals: Decimal = 6 // 30 / 5
  191. var deviation = (intervals * (minDelta - currentGlucoseImpact)).rounded(toPlaces: 0)
  192. if deviation < 0 {
  193. deviation = (intervals * (minAvgDelta - currentGlucoseImpact)).rounded(toPlaces: 0)
  194. if deviation < 0 {
  195. deviation = (intervals * (longAvgDelta - currentGlucoseImpact)).rounded(toPlaces: 0)
  196. }
  197. }
  198. // Calculate what oref calls "naive eventual glucose"
  199. guard let currentIob = iobData.first?.iob else {
  200. throw DeterminationError.missingIob
  201. }
  202. let naiveEventualGlucose: Decimal
  203. if currentIob > 0 {
  204. naiveEventualGlucose = (currentGlucose - (currentIob * adjustedSensitivity)).rounded(toPlaces: 0)
  205. } else {
  206. naiveEventualGlucose =
  207. (
  208. currentGlucose -
  209. (
  210. currentIob *
  211. min(
  212. profile.profileSensitivity(at: currentTime, trioCustomOrefVaribales: trioCustomOrefVariables),
  213. adjustedSensitivity
  214. )
  215. )
  216. )
  217. .rounded(toPlaces: 0)
  218. }
  219. let eventualGlucose = naiveEventualGlucose + deviation
  220. // Safety: if we ever get an invalid Decimal (very rare with Decimal), handle
  221. guard eventualGlucose.isFinite else {
  222. throw DeterminationError.eventualGlucoseCalculationError(sensitivity: adjustedSensitivity, deviation: deviation)
  223. }
  224. let forecastResult = ForecastGenerator.generate(
  225. glucose: currentGlucose,
  226. glucoseStatus: glucoseStatus,
  227. currentGlucoseImpact: currentGlucoseImpact,
  228. glucoseImpactSeries: glucoseImpactSeries,
  229. glucoseImpactSeriesWithZeroTemp: glucoseImpactSeriesWithZeroTemp,
  230. iobData: iobData,
  231. mealData: mealData,
  232. profile: profile,
  233. preferences: preferences,
  234. trioCustomOrefVariables: trioCustomOrefVariables,
  235. dynamicIsfResult: dynamicIsfResult,
  236. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  237. adjustedSensitivity: adjustedSensitivity,
  238. sensitivityRatio: sensitivityRatio,
  239. naiveEventualGlucose: naiveEventualGlucose,
  240. eventualGlucose: eventualGlucose,
  241. threshold: threshold,
  242. currentTime: currentTime
  243. )
  244. // used for pre dosing decision sanity later on
  245. let expectedDelta = calculateExpectedDelta(
  246. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  247. eventualGlucose: eventualGlucose,
  248. glucoseImpact: currentGlucoseImpact
  249. )
  250. let dosingInputs = DosingEngine.prepareDosingInputs(
  251. profile: profile,
  252. mealData: mealData,
  253. forecast: forecastResult,
  254. naiveEventualGlucose: naiveEventualGlucose,
  255. threshold: threshold,
  256. glucoseImpact: currentGlucoseImpact,
  257. deviation: deviation,
  258. currentBasal: profile.currentBasal ?? profile.basalFor(time: currentTime),
  259. overrideFactor: trioCustomOrefVariables.overrideFactor(),
  260. adjustedSensitivity: adjustedSensitivity,
  261. isfReason: "", // Placeholder
  262. tddReason: "", // Placeholder
  263. targetLog: "" // Placeholder
  264. )
  265. let smbDecision = try DosingEngine.makeSMBDosingDecision(
  266. profile: profile,
  267. meal: mealData,
  268. currentGlucose: currentGlucose,
  269. adjustedTargetGlucose: adjustedGlucoseTargets.targetGlucose,
  270. minGuardGlucose: forecastResult.minGuardGlucose,
  271. threshold: threshold,
  272. glucoseStatus: glucoseStatus,
  273. trioCustomOrefVariables: trioCustomOrefVariables,
  274. clock: currentTime
  275. )
  276. let smbIsEnabled = smbDecision.isEnabled
  277. var reason = dosingInputs.reason
  278. if let smbReason = smbDecision.reason {
  279. reason += smbReason
  280. }
  281. var determination = Determination(
  282. id: UUID(),
  283. reason: reason,
  284. units: nil,
  285. insulinReq: nil,
  286. eventualBG: Int(forecastResult.eventualGlucose.jsRounded()),
  287. sensitivityRatio: sensitivityRatio, // this would only the AS-adjusted one for now
  288. rate: nil,
  289. duration: nil,
  290. iob: iobData.first?.iob,
  291. cob: mealData.mealCOB,
  292. predictions: Predictions(
  293. iob: forecastResult.iob.map { Int($0.jsRounded()) },
  294. zt: forecastResult.zt.map { Int($0.jsRounded()) },
  295. cob: forecastResult.cob?.map { Int($0.jsRounded()) },
  296. uam: forecastResult.uam?.map { Int($0.jsRounded()) }
  297. ),
  298. deliverAt: currentTime,
  299. carbsReq: dosingInputs.carbsRequired?.carbs,
  300. temp: nil,
  301. bg: currentGlucose,
  302. reservoir: nil,
  303. isf: nil,
  304. timestamp: currentTime,
  305. tdd: nil,
  306. current_target: nil,
  307. minDelta: nil,
  308. expectedDelta: expectedDelta,
  309. minGuardBG: smbDecision.minGuardGlucose ?? forecastResult.minGuardGlucose,
  310. minPredBG: forecastResult.minForecastedGlucose,
  311. threshold: threshold.jsRounded(),
  312. carbRatio: forecastResult.adjustedCarbRatio.jsRounded(scale: 1),
  313. received: false
  314. )
  315. // MARK: - Core dosing logic
  316. let (shouldSetTempBasalForLowGlucoseSuspend, lowGlucoseSuspendDetermination) = try DosingEngine.lowGlucoseSuspend(
  317. currentGlucose: currentGlucose,
  318. minGuardGlucose: forecastResult.minGuardGlucose,
  319. iob: currentIob,
  320. minDelta: minDelta,
  321. expectedDelta: expectedDelta,
  322. threshold: threshold,
  323. overrideFactor: trioCustomOrefVariables.overrideFactor(),
  324. profile: profile,
  325. adjustedSensitivity: adjustedSensitivity,
  326. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  327. currentTemp: currentTemp,
  328. determination: determination
  329. )
  330. determination = lowGlucoseSuspendDetermination
  331. if shouldSetTempBasalForLowGlucoseSuspend {
  332. return determination
  333. }
  334. let (shouldSetTempBasalForSkipNeutralTemp, skipNeutralTempDetermination) = try DosingEngine.skipNeutralTempBasal(
  335. smbIsEnabled: smbIsEnabled,
  336. profile: profile,
  337. clock: currentTime,
  338. currentTemp: currentTemp,
  339. determination: determination
  340. )
  341. determination = skipNeutralTempDetermination
  342. if shouldSetTempBasalForSkipNeutralTemp {
  343. return determination
  344. }
  345. let (shouldSetTempBasalForLowEventualGlucose, lowEventualGlucoseDetermination) = try DosingEngine
  346. .handleLowEventualGlucose(
  347. eventualGlucose: forecastResult.eventualGlucose,
  348. minGlucose: adjustedGlucoseTargets.minGlucose,
  349. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  350. minDelta: minDelta,
  351. expectedDelta: expectedDelta,
  352. carbsRequired: dosingInputs.carbsRequired?.carbs ?? 0,
  353. naiveEventualGlucose: naiveEventualGlucose,
  354. glucoseStatus: glucoseStatus,
  355. currentTemp: currentTemp,
  356. basal: basal,
  357. profile: profile,
  358. determination: determination,
  359. adjustedSensitivity: adjustedSensitivity,
  360. overrideFactor: trioCustomOrefVariables.overrideFactor()
  361. )
  362. determination = lowEventualGlucoseDetermination
  363. if shouldSetTempBasalForLowEventualGlucose {
  364. return determination
  365. }
  366. let (
  367. shouldSetTempBasalForGlucoseFallingFasterThanExpected,
  368. glucoseFallingFasterThanExpectedDetermination
  369. ) = try DosingEngine.glucoseFallingFasterThanExpected(
  370. eventualGlucose: forecastResult.eventualGlucose,
  371. minGlucose: adjustedGlucoseTargets.minGlucose,
  372. minDelta: minDelta,
  373. expectedDelta: expectedDelta,
  374. glucoseStatus: glucoseStatus,
  375. currentTemp: currentTemp,
  376. basal: basal,
  377. smbIsEnabled: smbIsEnabled,
  378. profile: profile,
  379. determination: determination
  380. )
  381. determination = glucoseFallingFasterThanExpectedDetermination
  382. if shouldSetTempBasalForGlucoseFallingFasterThanExpected {
  383. return determination
  384. }
  385. let (
  386. shouldSetTempBasalEventualOrForecastGlucoseLessThanMax,
  387. eventualOrForecastGlucoseLessThanMaxDetermination
  388. ) = try DosingEngine.eventualOrForecastGlucoseLessThanMax(
  389. eventualGlucose: forecastResult.eventualGlucose,
  390. maxGlucose: adjustedGlucoseTargets.maxGlucose,
  391. minForecastGlucose: forecastResult.minForecastedGlucose,
  392. currentTemp: currentTemp,
  393. basal: basal,
  394. smbIsEnabled: smbIsEnabled,
  395. profile: profile,
  396. determination: determination
  397. )
  398. determination = eventualOrForecastGlucoseLessThanMaxDetermination
  399. if shouldSetTempBasalEventualOrForecastGlucoseLessThanMax {
  400. return determination
  401. }
  402. if forecastResult.eventualGlucose >= adjustedGlucoseTargets.maxGlucose {
  403. determination
  404. .reason +=
  405. "Eventual BG \(DosingEngine.convertGlucose(profile: profile, glucose: forecastResult.eventualGlucose)) >= \(DosingEngine.convertGlucose(profile: profile, glucose: adjustedGlucoseTargets.maxGlucose)), "
  406. }
  407. let (shouldSetTempBasalForIobGreaterThanMax, iobGreaterThanMaxDetermination) = try DosingEngine.iobGreaterThanMax(
  408. iob: currentIob,
  409. maxIob: profile.maxIob,
  410. currentTemp: currentTemp,
  411. basal: basal,
  412. profile: profile,
  413. determination: determination
  414. )
  415. determination = iobGreaterThanMaxDetermination
  416. if shouldSetTempBasalForIobGreaterThanMax {
  417. return determination
  418. }
  419. // MARK: - Aggressive dosing logic (SMB, High Temps)
  420. // Calculate Insulin Required
  421. let (insulinRequired, insulinReqDetermination) = DosingEngine.calculateInsulinRequired(
  422. minForecastGlucose: forecastResult.minForecastedGlucose,
  423. eventualGlucose: forecastResult.eventualGlucose,
  424. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  425. adjustedSensitivity: adjustedSensitivity,
  426. maxIob: profile.maxIob,
  427. currentIob: currentIob,
  428. determination: determination
  429. )
  430. determination = insulinReqDetermination
  431. // SMB Delivery
  432. let (shouldSetTempBasalForSMB, smbDetermination) = try DosingEngine.determineSMBDelivery(
  433. insulinRequired: insulinRequired,
  434. microBolusAllowed: microBolusAllowed,
  435. smbIsEnabled: smbIsEnabled,
  436. currentGlucose: currentGlucose,
  437. threshold: threshold,
  438. profile: profile,
  439. mealData: mealData,
  440. iobData: iobData,
  441. currentTime: currentTime,
  442. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  443. naiveEventualGlucose: naiveEventualGlucose,
  444. minIOBForecastedGlucose: forecastResult.iob.min() ?? 0,
  445. adjustedSensitivity: adjustedSensitivity,
  446. overrideFactor: trioCustomOrefVariables.overrideFactor(),
  447. adjustedCarbRatio: forecastResult.adjustedCarbRatio,
  448. basal: basal,
  449. determination: determination
  450. )
  451. determination = smbDetermination
  452. if shouldSetTempBasalForSMB {
  453. return determination
  454. }
  455. // High Temp Basal (Fallback)
  456. return try DosingEngine.determineHighTempBasal(
  457. insulinRequired: insulinRequired,
  458. basal: basal,
  459. profile: profile,
  460. currentTemp: currentTemp,
  461. determination: determination
  462. )
  463. }
  464. static func checkDeterminationInputs(
  465. glucoseStatus: GlucoseStatus?,
  466. currentTemp _: TempBasal?,
  467. iobData: [IobResult]?,
  468. profile: Profile?,
  469. trioCustomOrefVariables: TrioCustomOrefVariables,
  470. currentTime: Date = Date()
  471. ) throws {
  472. guard let glucoseStatus = glucoseStatus else {
  473. throw DeterminationError.missingGlucoseStatus
  474. }
  475. guard let profile = profile else {
  476. throw DeterminationError.missingProfile
  477. }
  478. guard profile.profileTarget(trioCustomOrefVariables: trioCustomOrefVariables) != nil else {
  479. throw DeterminationError.invalidProfileTarget
  480. }
  481. let glucoseAge = currentTime.timeIntervalSince(glucoseStatus.date)
  482. if glucoseAge > 15 * 60 {
  483. throw DeterminationError.staleGlucoseData(ageMinutes: glucoseAge / 60)
  484. }
  485. // we have to allow 38 values so that we can cancel high temps
  486. if glucoseStatus.glucose < 38 || glucoseStatus.glucose > 600 {
  487. throw DeterminationError.glucoseOutOfRange(glucose: glucoseStatus.glucose)
  488. }
  489. guard let _ = iobData else {
  490. throw DeterminationError.missingIob
  491. }
  492. }
  493. static func handleTempBasalCases(
  494. glucoseStatus: GlucoseStatus,
  495. profile: Profile,
  496. currentTemp: TempBasal?,
  497. currentTime: Date,
  498. trioCustomOrefVariables: TrioCustomOrefVariables
  499. ) throws -> Determination? {
  500. let glucose = glucoseStatus.glucose
  501. let noise = glucoseStatus.noise
  502. let bgTime = glucoseStatus.date
  503. let minAgo = Decimal(currentTime.timeIntervalSince(bgTime) / 60) // minutes
  504. let shortAvgDelta = glucoseStatus.shortAvgDelta
  505. let longAvgDelta = glucoseStatus.longAvgDelta
  506. let delta = glucoseStatus.delta
  507. let device = glucoseStatus.device
  508. // Always use profile-supplied basal
  509. guard let profileBasal = profile.currentBasal else {
  510. throw DeterminationError.missingCurrentBasal
  511. }
  512. let basal = profileBasal * trioCustomOrefVariables.overrideFactor()
  513. // Compose tick for log
  514. let tick: String = (delta > -0.5) ? "+\(delta.rounded(toPlaces: 0))" : "\(delta.rounded(toPlaces: 0))"
  515. let minDelta = min(delta, shortAvgDelta)
  516. let minAvgDelta = min(shortAvgDelta, longAvgDelta)
  517. let maxDelta = max(delta, shortAvgDelta, longAvgDelta)
  518. var reason = ""
  519. // === ERROR CONDITIONS ===
  520. // xDrip code 38 = sensor error; BG <= 10 = ???/calibrating; noise >= 3 = high noise
  521. if glucose <= 10 || glucose == 38 || noise >= 3 {
  522. reason = "CGM is calibrating, in ??? state, or noise is high"
  523. }
  524. // minAgo (BG age) > 12 or < -5 = old/future BG
  525. if minAgo > 12 || minAgo < -5 {
  526. reason =
  527. "If current system time \(currentTime) is correct, then BG data is too old. The last BG data was read \(minAgo) min ago at \(bgTime)"
  528. }
  529. // CGM data unchanged (flat)
  530. if shortAvgDelta == 0 && longAvgDelta == 0 {
  531. if glucoseStatus.lastCalIndex != nil, glucoseStatus.lastCalIndex! < 3 {
  532. reason = "CGM was just calibrated"
  533. } else {
  534. reason =
  535. "CGM data is unchanged (\(glucose)+\(delta)) for 5m w/ \(shortAvgDelta) mg/dL ~15m change & \(longAvgDelta) mg/dL ~45m change"
  536. }
  537. }
  538. let errorDetected =
  539. glucose <= 10 ||
  540. glucose == 38 ||
  541. noise >= 3 ||
  542. minAgo > 12 ||
  543. minAgo < -5
  544. // === IF ERROR, CANCEL/SHORTEN TEMPS ===
  545. guard errorDetected, let currentTemp = currentTemp else { return nil }
  546. if currentTemp.rate >= basal { // high temp is running
  547. // Replace high temp with neutral temp at scheduled basal rate for 30min
  548. let reasonWithAction = reason +
  549. ". Replacing high temp basal of \(currentTemp.rate)U/hr with neutral temp of \(basal)U/hr"
  550. return Determination(
  551. id: UUID(),
  552. reason: reasonWithAction,
  553. units: nil,
  554. insulinReq: nil,
  555. eventualBG: nil,
  556. sensitivityRatio: nil,
  557. rate: basal,
  558. duration: 30,
  559. iob: nil,
  560. cob: nil,
  561. predictions: nil,
  562. deliverAt: currentTime,
  563. carbsReq: nil,
  564. temp: .absolute,
  565. bg: glucose,
  566. reservoir: nil,
  567. isf: profile.sens,
  568. timestamp: currentTime,
  569. tdd: nil,
  570. current_target: profile.targetBg,
  571. minDelta: minDelta,
  572. expectedDelta: nil,
  573. minGuardBG: nil,
  574. minPredBG: nil,
  575. threshold: nil,
  576. carbRatio: profile.carbRatio,
  577. received: false
  578. )
  579. } else if currentTemp.rate == 0, currentTemp.duration > 30 {
  580. // Shorten long zero temp to 30m
  581. let reasonWithAction = reason + ". Shortening \(currentTemp.duration)m long zero temp to 30m."
  582. return Determination(
  583. id: UUID(),
  584. reason: reasonWithAction,
  585. units: nil,
  586. insulinReq: nil,
  587. eventualBG: nil,
  588. sensitivityRatio: nil,
  589. rate: 0,
  590. duration: 30,
  591. iob: nil,
  592. cob: nil,
  593. predictions: nil,
  594. deliverAt: currentTime,
  595. carbsReq: nil,
  596. temp: .absolute,
  597. bg: glucose,
  598. reservoir: nil,
  599. isf: profile.sens,
  600. timestamp: currentTime,
  601. tdd: nil,
  602. current_target: profile.targetBg,
  603. minDelta: minDelta,
  604. expectedDelta: nil,
  605. minGuardBG: nil,
  606. minPredBG: nil,
  607. threshold: nil,
  608. carbRatio: profile.carbRatio,
  609. received: false
  610. )
  611. } else {
  612. // Do nothing (temp already safe)
  613. let reasonWithAction = reason + ". Temp \(currentTemp.rate) <= current basal \(basal)U/hr; doing nothing."
  614. return Determination(
  615. id: UUID(),
  616. reason: reasonWithAction,
  617. units: nil,
  618. insulinReq: nil,
  619. eventualBG: nil,
  620. sensitivityRatio: nil,
  621. rate: currentTemp.rate,
  622. duration: Decimal(currentTemp.duration),
  623. iob: nil,
  624. cob: nil,
  625. predictions: nil,
  626. deliverAt: currentTime,
  627. carbsReq: nil,
  628. temp: currentTemp.temp,
  629. bg: glucose,
  630. reservoir: nil,
  631. isf: profile.sens,
  632. timestamp: currentTime,
  633. tdd: nil,
  634. current_target: profile.targetBg,
  635. minDelta: minDelta,
  636. expectedDelta: nil,
  637. minGuardBG: nil,
  638. minPredBG: nil,
  639. threshold: nil,
  640. carbRatio: profile.carbRatio,
  641. received: false
  642. )
  643. }
  644. }
  645. }