DetermineBasalEarlyExitTests.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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. "Warning: currenttemp rate 1.5 != lastTemp rate 1 from pumphistory; canceling temp"
  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 == "Warning: currenttemp running but lastTemp from pumphistory ended 10m ago; canceling temp"
  414. )
  415. }
  416. // Test 8 from JS
  417. @Test("should throw error if eventualBG cannot be calculated") func eventualBGNaN() throws {
  418. var (
  419. profile,
  420. preferences,
  421. currentTemp,
  422. iobData,
  423. mealData,
  424. autosensData,
  425. reservoirData,
  426. glucoseStatus,
  427. microBolusAllowed,
  428. trioCustomOrefVariables,
  429. currentTime
  430. ) = createDefaultInputs()
  431. profile.sens = .nan
  432. #expect(throws: DeterminationError.eventualGlucoseCalculationError(sensitivity: .nan, deviation: .nan)) {
  433. _ = try DeterminationGenerator.determineBasal(
  434. profile: profile,
  435. preferences: preferences,
  436. currentTemp: currentTemp,
  437. iobData: iobData,
  438. mealData: mealData,
  439. autosensData: autosensData,
  440. reservoirData: reservoirData,
  441. glucoseStatus: glucoseStatus,
  442. microBolusAllowed: microBolusAllowed,
  443. trioCustomOrefVariables: trioCustomOrefVariables,
  444. currentTime: currentTime
  445. )
  446. }
  447. }
  448. // Test 9 from JS
  449. @Test("should low-temp if BG is below threshold") func lowGlucoseSuspend() throws {
  450. let (
  451. profile,
  452. preferences,
  453. currentTemp,
  454. iobData,
  455. mealData,
  456. autosensData,
  457. reservoirData,
  458. _,
  459. microBolusAllowed,
  460. trioCustomOrefVariables,
  461. currentTime
  462. ) = createDefaultInputs()
  463. let glucoseStatus = GlucoseStatus(
  464. delta: 0,
  465. glucose: 70,
  466. noise: 1,
  467. shortAvgDelta: 0,
  468. longAvgDelta: 0.1,
  469. date: currentTime,
  470. lastCalIndex: nil,
  471. device: "test"
  472. )
  473. let result = try DeterminationGenerator.determineBasal(
  474. profile: profile,
  475. preferences: preferences,
  476. currentTemp: currentTemp,
  477. iobData: iobData,
  478. mealData: mealData,
  479. autosensData: autosensData,
  480. reservoirData: reservoirData,
  481. glucoseStatus: glucoseStatus,
  482. microBolusAllowed: microBolusAllowed,
  483. trioCustomOrefVariables: trioCustomOrefVariables,
  484. currentTime: currentTime
  485. )
  486. #expect(result?.rate == 0)
  487. #expect((result?.duration ?? 0) >= 30)
  488. #expect(result?.reason.contains("minGuardBG") == true)
  489. }
  490. // Test 10 from JS
  491. @Test("should cancel temp before the hour if not doing SMB") func skipNeutralTemp() throws {
  492. // Create a date that is 56 minutes past the hour
  493. var components = Calendar.current.dateComponents(in: .current, from: Date())
  494. components.minute = 56
  495. let currentTime = Calendar.current.date(from: components)!
  496. var (
  497. profile,
  498. preferences,
  499. currentTemp,
  500. iobData,
  501. mealData,
  502. autosensData,
  503. reservoirData,
  504. glucoseStatus,
  505. microBolusAllowed,
  506. trioCustomOrefVariables,
  507. _
  508. ) = createDefaultInputs(currentTime: currentTime)
  509. profile.skipNeutralTemps = true
  510. let result = try DeterminationGenerator.determineBasal(
  511. profile: profile,
  512. preferences: preferences,
  513. currentTemp: currentTemp,
  514. iobData: iobData,
  515. mealData: mealData,
  516. autosensData: autosensData,
  517. reservoirData: reservoirData,
  518. glucoseStatus: glucoseStatus,
  519. microBolusAllowed: microBolusAllowed,
  520. trioCustomOrefVariables: trioCustomOrefVariables,
  521. currentTime: currentTime
  522. )
  523. #expect(result?.rate == 0)
  524. #expect(result?.duration == 0)
  525. #expect(result?.reason.contains("Canceling temp") == true)
  526. }
  527. }