DetermineBasalGenerator.swift 29 KB

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