DetermineBasalEarlyExitTests.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("DetermineBasal early exits before core dosing logic") struct DetermineBasalEarlyExitTests {
  5. private func createDefaultInputs(currentTime: Date = Date()) -> (
  6. profile: Profile,
  7. preferences: Preferences,
  8. currentTemp: TempBasal,
  9. iobData: [IobResult],
  10. mealData: ComputedCarbs,
  11. autosensData: Autosens,
  12. reservoirData: Decimal,
  13. glucoseStatus: GlucoseStatus,
  14. microBolusAllowed: Bool,
  15. trioCustomOrefVariables: TrioCustomOrefVariables,
  16. currentTime: Date
  17. ) {
  18. var profile = Profile()
  19. profile.maxIob = 2.5
  20. profile.dia = 3
  21. profile.currentBasal = 0.9
  22. profile.maxDailyBasal = 1.3
  23. profile.maxBasal = 3.5
  24. profile.maxBg = 120
  25. profile.minBg = 110
  26. profile.sens = 40
  27. profile.carbRatio = 10
  28. profile.thresholdSetting = 80
  29. profile.temptargetSet = false
  30. profile.bolusIncrement = 0.1
  31. profile.useCustomPeakTime = false
  32. profile.curve = .rapidActing
  33. var preferences = Preferences()
  34. preferences.useNewFormula = false
  35. preferences.sigmoid = false
  36. preferences.adjustmentFactor = 0.8
  37. preferences.adjustmentFactorSigmoid = 0.5
  38. preferences.curve = .rapidActing
  39. preferences.useCustomPeakTime = false
  40. let currentTemp = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: currentTime)
  41. let iobData = [IobResult(
  42. iob: 0,
  43. activity: 0,
  44. basaliob: 0,
  45. bolusiob: 0,
  46. netbasalinsulin: 0,
  47. bolusinsulin: 0,
  48. time: currentTime,
  49. iobWithZeroTemp: IobResult.IobWithZeroTemp(
  50. iob: 0,
  51. activity: 0,
  52. basaliob: 0,
  53. bolusiob: 0,
  54. netbasalinsulin: 0,
  55. bolusinsulin: 0,
  56. time: currentTime
  57. ),
  58. lastBolusTime: nil,
  59. lastTemp: IobResult.LastTemp(
  60. rate: 0,
  61. timestamp: currentTime,
  62. started_at: currentTime,
  63. date: UInt64(currentTime.timeIntervalSince1970 * 1000),
  64. duration: 30
  65. )
  66. )]
  67. let mealData = ComputedCarbs(
  68. carbs: 0,
  69. mealCOB: 0,
  70. currentDeviation: 0,
  71. maxDeviation: 0,
  72. minDeviation: 0,
  73. slopeFromMaxDeviation: 0,
  74. slopeFromMinDeviation: 0,
  75. allDeviations: [0, 0, 0, 0, 0],
  76. lastCarbTime: 0
  77. )
  78. let autosensData = Autosens(ratio: 1.0, newisf: nil)
  79. let glucoseStatus = GlucoseStatus(
  80. delta: 0,
  81. glucose: 115,
  82. noise: 1,
  83. shortAvgDelta: 0,
  84. longAvgDelta: 0.1,
  85. date: currentTime,
  86. lastCalIndex: nil,
  87. device: "test"
  88. )
  89. let trioCustomOrefVariables = TrioCustomOrefVariables(
  90. average_total_data: 0,
  91. weightedAverage: 0,
  92. currentTDD: 0,
  93. past2hoursAverage: 0,
  94. date: currentTime,
  95. overridePercentage: 100,
  96. useOverride: false,
  97. duration: 0,
  98. unlimited: false,
  99. overrideTarget: 0,
  100. smbIsOff: false,
  101. advancedSettings: false,
  102. isfAndCr: false,
  103. isf: false,
  104. cr: false,
  105. smbIsScheduledOff: false,
  106. start: 0,
  107. end: 0,
  108. smbMinutes: 30,
  109. uamMinutes: 30
  110. )
  111. return (
  112. profile: profile,
  113. preferences: preferences,
  114. currentTemp: currentTemp,
  115. iobData: iobData,
  116. mealData: mealData,
  117. autosensData: autosensData,
  118. reservoirData: 100,
  119. glucoseStatus: glucoseStatus,
  120. microBolusAllowed: true,
  121. trioCustomOrefVariables: trioCustomOrefVariables,
  122. currentTime: currentTime
  123. )
  124. }
  125. // Test 1 from JS
  126. @Test("should fail if current_basal is missing") func missingCurrentBasal() throws {
  127. var (
  128. profile,
  129. preferences,
  130. currentTemp,
  131. iobData,
  132. mealData,
  133. autosensData,
  134. reservoirData,
  135. glucoseStatus,
  136. microBolusAllowed,
  137. trioCustomOrefVariables,
  138. currentTime
  139. ) = createDefaultInputs()
  140. profile.currentBasal = nil
  141. profile.basalprofile = [] // ensure basalFor also returns nil
  142. #expect(throws: DeterminationError.missingCurrentBasal) {
  143. _ = try DeterminationGenerator.determineBasal(
  144. profile: profile,
  145. preferences: preferences,
  146. currentTemp: currentTemp,
  147. iobData: iobData,
  148. mealData: mealData,
  149. autosensData: autosensData,
  150. reservoirData: reservoirData,
  151. glucoseStatus: glucoseStatus,
  152. microBolusAllowed: microBolusAllowed,
  153. trioCustomOrefVariables: trioCustomOrefVariables,
  154. currentTime: currentTime
  155. )
  156. }
  157. }
  158. // Test 2 from JS
  159. @Test("should cancel high temp if BG is 38") func cancelHighTempBG38() throws {
  160. let (
  161. profile,
  162. preferences,
  163. _,
  164. iobData,
  165. mealData,
  166. autosensData,
  167. reservoirData,
  168. _,
  169. microBolusAllowed,
  170. trioCustomOrefVariables,
  171. currentTime
  172. ) = createDefaultInputs()
  173. let glucoseStatus = GlucoseStatus(
  174. delta: 0,
  175. glucose: 38,
  176. noise: 1,
  177. shortAvgDelta: 0,
  178. longAvgDelta: 0.1,
  179. date: currentTime,
  180. lastCalIndex: nil,
  181. device: "test"
  182. )
  183. let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
  184. let result = try DeterminationGenerator.determineBasal(
  185. profile: profile,
  186. preferences: preferences,
  187. currentTemp: currentTemp,
  188. iobData: iobData,
  189. mealData: mealData,
  190. autosensData: autosensData,
  191. reservoirData: reservoirData,
  192. glucoseStatus: glucoseStatus,
  193. microBolusAllowed: microBolusAllowed,
  194. trioCustomOrefVariables: trioCustomOrefVariables,
  195. currentTime: currentTime
  196. )
  197. #expect(result?.rate == profile.currentBasal)
  198. #expect(result?.duration == 30)
  199. #expect(result?.reason.contains("Replacing high temp basal") == true)
  200. }
  201. // Test 3 from JS
  202. @Test("should shorten long zero temp if BG data is too old") func shortenLongZeroTempTooOldBG() throws {
  203. let (
  204. profile,
  205. preferences,
  206. _,
  207. iobData,
  208. mealData,
  209. autosensData,
  210. reservoirData,
  211. _,
  212. microBolusAllowed,
  213. trioCustomOrefVariables,
  214. currentTime
  215. ) = createDefaultInputs()
  216. let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
  217. let glucoseStatus = GlucoseStatus(
  218. delta: 0,
  219. glucose: 115,
  220. noise: 1,
  221. shortAvgDelta: 0,
  222. longAvgDelta: 0.1,
  223. date: glucoseTime,
  224. lastCalIndex: nil,
  225. device: "test"
  226. )
  227. let currentTemp = TempBasal(duration: 60, rate: 0, temp: .absolute, timestamp: currentTime)
  228. let result = try DeterminationGenerator.determineBasal(
  229. profile: profile,
  230. preferences: preferences,
  231. currentTemp: currentTemp,
  232. iobData: iobData,
  233. mealData: mealData,
  234. autosensData: autosensData,
  235. reservoirData: reservoirData,
  236. glucoseStatus: glucoseStatus,
  237. microBolusAllowed: microBolusAllowed,
  238. trioCustomOrefVariables: trioCustomOrefVariables,
  239. currentTime: currentTime
  240. )
  241. #expect(result?.rate == 0)
  242. #expect(result?.duration == 30)
  243. #expect(result?.reason.contains("Shortening") == true)
  244. }
  245. // Test 4 from JS
  246. @Test("should do nothing if BG is too old and temp is not high") func doNothingOldBGNotHighTemp() throws {
  247. let (
  248. profile,
  249. preferences,
  250. _,
  251. iobData,
  252. mealData,
  253. autosensData,
  254. reservoirData,
  255. _,
  256. microBolusAllowed,
  257. trioCustomOrefVariables,
  258. currentTime
  259. ) = createDefaultInputs()
  260. let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
  261. let glucoseStatus = GlucoseStatus(
  262. delta: 0,
  263. glucose: 115,
  264. noise: 1,
  265. shortAvgDelta: 0,
  266. longAvgDelta: 0.1,
  267. date: glucoseTime,
  268. lastCalIndex: nil,
  269. device: "test"
  270. )
  271. let currentTemp = TempBasal(duration: 30, rate: 0.5, temp: .absolute, timestamp: currentTime)
  272. let result = try DeterminationGenerator.determineBasal(
  273. profile: profile,
  274. preferences: preferences,
  275. currentTemp: currentTemp,
  276. iobData: iobData,
  277. mealData: mealData,
  278. autosensData: autosensData,
  279. reservoirData: reservoirData,
  280. glucoseStatus: glucoseStatus,
  281. microBolusAllowed: microBolusAllowed,
  282. trioCustomOrefVariables: trioCustomOrefVariables,
  283. currentTime: currentTime
  284. )
  285. #expect(result?.rate == nil)
  286. #expect(result?.duration == nil)
  287. #expect(result?.reason.contains("doing nothing") == true)
  288. }
  289. // Test 5 from JS
  290. @Test("should error if target_bg cannot be determined") func errorIfTargetBGMissing() throws {
  291. var (
  292. profile,
  293. preferences,
  294. currentTemp,
  295. iobData,
  296. mealData,
  297. autosensData,
  298. reservoirData,
  299. glucoseStatus,
  300. microBolusAllowed,
  301. trioCustomOrefVariables,
  302. currentTime
  303. ) = createDefaultInputs()
  304. profile.minBg = nil
  305. #expect(throws: DeterminationError.invalidProfileTarget) {
  306. _ = try DeterminationGenerator.determineBasal(
  307. profile: profile,
  308. preferences: preferences,
  309. currentTemp: currentTemp,
  310. iobData: iobData,
  311. mealData: mealData,
  312. autosensData: autosensData,
  313. reservoirData: reservoirData,
  314. glucoseStatus: glucoseStatus,
  315. microBolusAllowed: microBolusAllowed,
  316. trioCustomOrefVariables: trioCustomOrefVariables,
  317. currentTime: currentTime
  318. )
  319. }
  320. }
  321. // Test 6 from JS
  322. @Test("should cancel temp if currenttemp and lastTemp from pumphistory do not match") func cancelTempMismatch() throws {
  323. let (
  324. profile,
  325. preferences,
  326. _,
  327. iobData,
  328. mealData,
  329. autosensData,
  330. reservoirData,
  331. glucoseStatus,
  332. microBolusAllowed,
  333. trioCustomOrefVariables,
  334. currentTime
  335. ) = createDefaultInputs()
  336. let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
  337. let lastTempTime = currentTime.addingTimeInterval(-15 * 60)
  338. let lastTemp = IobResult.LastTemp(
  339. rate: 1.0,
  340. timestamp: lastTempTime,
  341. started_at: lastTempTime,
  342. date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
  343. duration: 30
  344. )
  345. var mutableIobData = iobData
  346. mutableIobData[0].lastTemp = lastTemp
  347. let result = try DeterminationGenerator.determineBasal(
  348. profile: profile,
  349. preferences: preferences,
  350. currentTemp: currentTemp,
  351. iobData: mutableIobData,
  352. mealData: mealData,
  353. autosensData: autosensData,
  354. reservoirData: reservoirData,
  355. glucoseStatus: glucoseStatus,
  356. microBolusAllowed: microBolusAllowed,
  357. trioCustomOrefVariables: trioCustomOrefVariables,
  358. currentTime: currentTime
  359. )
  360. #expect(result?.rate == 0)
  361. #expect(result?.duration == 0)
  362. // Note: In swift we use a different reason then JS
  363. #expect(
  364. result?
  365. .reason ==
  366. "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
  367. )
  368. }
  369. // Test 7 from JS
  370. @Test("should cancel temp if lastTemp from pumphistory ended long ago") func cancelTempOldLastTemp() throws {
  371. let (
  372. profile,
  373. preferences,
  374. _,
  375. iobData,
  376. mealData,
  377. autosensData,
  378. reservoirData,
  379. glucoseStatus,
  380. microBolusAllowed,
  381. trioCustomOrefVariables,
  382. currentTime
  383. ) = createDefaultInputs()
  384. let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
  385. let lastTempTime = currentTime.addingTimeInterval(-40 * 60)
  386. let lastTemp = IobResult.LastTemp(
  387. rate: 1.5,
  388. timestamp: lastTempTime,
  389. started_at: lastTempTime,
  390. date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
  391. duration: 30
  392. )
  393. var mutableIobData = iobData
  394. mutableIobData[0].lastTemp = lastTemp
  395. let result = try DeterminationGenerator.determineBasal(
  396. profile: profile,
  397. preferences: preferences,
  398. currentTemp: currentTemp,
  399. iobData: mutableIobData,
  400. mealData: mealData,
  401. autosensData: autosensData,
  402. reservoirData: reservoirData,
  403. glucoseStatus: glucoseStatus,
  404. microBolusAllowed: microBolusAllowed,
  405. trioCustomOrefVariables: trioCustomOrefVariables,
  406. currentTime: currentTime
  407. )
  408. #expect(result?.rate == 0)
  409. #expect(result?.duration == 0)
  410. // Note: In swift we use a different reason then JS
  411. #expect(
  412. result?
  413. .reason ==
  414. "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
  415. )
  416. }
  417. // Test 8 from JS
  418. @Test("should throw error if eventualBG cannot be calculated") func eventualBGNaN() throws {
  419. var (
  420. profile,
  421. preferences,
  422. currentTemp,
  423. iobData,
  424. mealData,
  425. autosensData,
  426. reservoirData,
  427. glucoseStatus,
  428. microBolusAllowed,
  429. trioCustomOrefVariables,
  430. currentTime
  431. ) = createDefaultInputs()
  432. profile.sens = .nan
  433. #expect(throws: DeterminationError.eventualGlucoseCalculationError(sensitivity: .nan, deviation: .nan)) {
  434. _ = try DeterminationGenerator.determineBasal(
  435. profile: profile,
  436. preferences: preferences,
  437. currentTemp: currentTemp,
  438. iobData: iobData,
  439. mealData: mealData,
  440. autosensData: autosensData,
  441. reservoirData: reservoirData,
  442. glucoseStatus: glucoseStatus,
  443. microBolusAllowed: microBolusAllowed,
  444. trioCustomOrefVariables: trioCustomOrefVariables,
  445. currentTime: currentTime
  446. )
  447. }
  448. }
  449. // Test 9 from JS
  450. @Test("should low-temp if BG is below threshold") func lowGlucoseSuspend() throws {
  451. let (
  452. profile,
  453. preferences,
  454. currentTemp,
  455. iobData,
  456. mealData,
  457. autosensData,
  458. reservoirData,
  459. _,
  460. microBolusAllowed,
  461. trioCustomOrefVariables,
  462. currentTime
  463. ) = createDefaultInputs()
  464. let glucoseStatus = GlucoseStatus(
  465. delta: 0,
  466. glucose: 70,
  467. noise: 1,
  468. shortAvgDelta: 0,
  469. longAvgDelta: 0.1,
  470. date: currentTime,
  471. lastCalIndex: nil,
  472. device: "test"
  473. )
  474. let result = try DeterminationGenerator.determineBasal(
  475. profile: profile,
  476. preferences: preferences,
  477. currentTemp: currentTemp,
  478. iobData: iobData,
  479. mealData: mealData,
  480. autosensData: autosensData,
  481. reservoirData: reservoirData,
  482. glucoseStatus: glucoseStatus,
  483. microBolusAllowed: microBolusAllowed,
  484. trioCustomOrefVariables: trioCustomOrefVariables,
  485. currentTime: currentTime
  486. )
  487. #expect(result?.rate == 0)
  488. #expect((result?.duration ?? 0) >= 30)
  489. #expect(result?.reason.contains("minGuardBG") == true)
  490. }
  491. // Test 10 from JS
  492. @Test("should cancel temp before the hour if not doing SMB") func skipNeutralTemp() throws {
  493. // Create a date that is 56 minutes past the hour
  494. var components = Calendar.current.dateComponents(in: .current, from: Date())
  495. components.minute = 56
  496. let currentTime = Calendar.current.date(from: components)!
  497. var (
  498. profile,
  499. preferences,
  500. currentTemp,
  501. iobData,
  502. mealData,
  503. autosensData,
  504. reservoirData,
  505. glucoseStatus,
  506. microBolusAllowed,
  507. trioCustomOrefVariables,
  508. _
  509. ) = createDefaultInputs(currentTime: currentTime)
  510. profile.skipNeutralTemps = true
  511. let result = try DeterminationGenerator.determineBasal(
  512. profile: profile,
  513. preferences: preferences,
  514. currentTemp: currentTemp,
  515. iobData: iobData,
  516. mealData: mealData,
  517. autosensData: autosensData,
  518. reservoirData: reservoirData,
  519. glucoseStatus: glucoseStatus,
  520. microBolusAllowed: microBolusAllowed,
  521. trioCustomOrefVariables: trioCustomOrefVariables,
  522. currentTime: currentTime
  523. )
  524. #expect(result?.rate == 0)
  525. #expect(result?.duration == 0)
  526. #expect(result?.reason.contains("Canceling temp") == true)
  527. }
  528. }