DetermineBasalEarlyExitTests.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  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() -> (
  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. trioCustomOrefVariables: TrioCustomOrefVariables,
  15. currentTime: Date
  16. ) {
  17. let currentTime = Date()
  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. shouldProtectDueToHIGH: false
  111. )
  112. return (
  113. profile: profile,
  114. preferences: preferences,
  115. currentTemp: currentTemp,
  116. iobData: iobData,
  117. mealData: mealData,
  118. autosensData: autosensData,
  119. reservoirData: 100,
  120. glucoseStatus: glucoseStatus,
  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. trioCustomOrefVariables,
  137. currentTime
  138. ) = createDefaultInputs()
  139. profile.currentBasal = nil
  140. profile.basalprofile = [] // ensure basalFor also returns nil
  141. #expect(throws: DeterminationError.missingCurrentBasal) {
  142. _ = try DeterminationGenerator.determineBasal(
  143. profile: profile,
  144. preferences: preferences,
  145. currentTemp: currentTemp,
  146. iobData: iobData,
  147. mealData: mealData,
  148. autosensData: autosensData,
  149. reservoirData: reservoirData,
  150. glucoseStatus: glucoseStatus,
  151. trioCustomOrefVariables: trioCustomOrefVariables,
  152. currentTime: currentTime
  153. )
  154. }
  155. }
  156. // Test 2 from JS
  157. @Test("should cancel high temp if BG is 38") func cancelHighTempBG38() throws {
  158. let (
  159. profile,
  160. preferences,
  161. _,
  162. iobData,
  163. mealData,
  164. autosensData,
  165. reservoirData,
  166. _,
  167. trioCustomOrefVariables,
  168. currentTime
  169. ) = createDefaultInputs()
  170. let glucoseStatus = GlucoseStatus(
  171. delta: 0,
  172. glucose: 38,
  173. noise: 1,
  174. shortAvgDelta: 0,
  175. longAvgDelta: 0.1,
  176. date: currentTime,
  177. lastCalIndex: nil,
  178. device: "test"
  179. )
  180. let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
  181. let result = try DeterminationGenerator.determineBasal(
  182. profile: profile,
  183. preferences: preferences,
  184. currentTemp: currentTemp,
  185. iobData: iobData,
  186. mealData: mealData,
  187. autosensData: autosensData,
  188. reservoirData: reservoirData,
  189. glucoseStatus: glucoseStatus,
  190. trioCustomOrefVariables: trioCustomOrefVariables,
  191. currentTime: currentTime
  192. )
  193. #expect(result?.rate == 0)
  194. #expect(result?.duration == 0)
  195. #expect(result?.reason.contains("Canceling high temp basal") == true)
  196. }
  197. // Test 3 from JS
  198. @Test("should shorten long zero temp if BG data is too old") func shortenLongZeroTempTooOldBG() throws {
  199. let (
  200. profile,
  201. preferences,
  202. _,
  203. iobData,
  204. mealData,
  205. autosensData,
  206. reservoirData,
  207. _,
  208. trioCustomOrefVariables,
  209. currentTime
  210. ) = createDefaultInputs()
  211. let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
  212. let glucoseStatus = GlucoseStatus(
  213. delta: 0,
  214. glucose: 115,
  215. noise: 1,
  216. shortAvgDelta: 0,
  217. longAvgDelta: 0.1,
  218. date: glucoseTime,
  219. lastCalIndex: nil,
  220. device: "test"
  221. )
  222. let currentTemp = TempBasal(duration: 60, rate: 0, temp: .absolute, timestamp: currentTime)
  223. let result = try DeterminationGenerator.determineBasal(
  224. profile: profile,
  225. preferences: preferences,
  226. currentTemp: currentTemp,
  227. iobData: iobData,
  228. mealData: mealData,
  229. autosensData: autosensData,
  230. reservoirData: reservoirData,
  231. glucoseStatus: glucoseStatus,
  232. trioCustomOrefVariables: trioCustomOrefVariables,
  233. currentTime: currentTime
  234. )
  235. #expect(result?.rate == 0)
  236. #expect(result?.duration == 30)
  237. #expect(result?.reason.contains("Shortening") == true)
  238. }
  239. // Test 4 from JS
  240. @Test("should do nothing if BG is too old and temp is not high") func doNothingOldBGNotHighTemp() throws {
  241. let (
  242. profile,
  243. preferences,
  244. _,
  245. iobData,
  246. mealData,
  247. autosensData,
  248. reservoirData,
  249. _,
  250. trioCustomOrefVariables,
  251. currentTime
  252. ) = createDefaultInputs()
  253. let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
  254. let glucoseStatus = GlucoseStatus(
  255. delta: 0,
  256. glucose: 115,
  257. noise: 1,
  258. shortAvgDelta: 0,
  259. longAvgDelta: 0.1,
  260. date: glucoseTime,
  261. lastCalIndex: nil,
  262. device: "test"
  263. )
  264. let currentTemp = TempBasal(duration: 30, rate: 0.5, temp: .absolute, timestamp: currentTime)
  265. let result = try DeterminationGenerator.determineBasal(
  266. profile: profile,
  267. preferences: preferences,
  268. currentTemp: currentTemp,
  269. iobData: iobData,
  270. mealData: mealData,
  271. autosensData: autosensData,
  272. reservoirData: reservoirData,
  273. glucoseStatus: glucoseStatus,
  274. trioCustomOrefVariables: trioCustomOrefVariables,
  275. currentTime: currentTime
  276. )
  277. #expect(result?.rate == 0.5)
  278. #expect(result?.duration == 30)
  279. #expect(result?.reason.contains("doing nothing") == true)
  280. }
  281. // Test 5 from JS
  282. @Test("should error if target_bg cannot be determined") func errorIfTargetBGMissing() throws {
  283. var (
  284. profile,
  285. preferences,
  286. currentTemp,
  287. iobData,
  288. mealData,
  289. autosensData,
  290. reservoirData,
  291. glucoseStatus,
  292. trioCustomOrefVariables,
  293. currentTime
  294. ) = createDefaultInputs()
  295. profile.minBg = nil
  296. #expect(throws: DeterminationError.invalidProfileTarget) {
  297. _ = try DeterminationGenerator.determineBasal(
  298. profile: profile,
  299. preferences: preferences,
  300. currentTemp: currentTemp,
  301. iobData: iobData,
  302. mealData: mealData,
  303. autosensData: autosensData,
  304. reservoirData: reservoirData,
  305. glucoseStatus: glucoseStatus,
  306. trioCustomOrefVariables: trioCustomOrefVariables,
  307. currentTime: currentTime
  308. )
  309. }
  310. }
  311. // Test 6 from JS
  312. @Test("should cancel temp if currenttemp and lastTemp from pumphistory do not match") func cancelTempMismatch() throws {
  313. let (
  314. profile,
  315. preferences,
  316. _,
  317. iobData,
  318. mealData,
  319. autosensData,
  320. reservoirData,
  321. glucoseStatus,
  322. trioCustomOrefVariables,
  323. currentTime
  324. ) = createDefaultInputs()
  325. let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
  326. let lastTempTime = currentTime.addingTimeInterval(-15 * 60)
  327. let lastTemp = IobResult.LastTemp(
  328. rate: 1.0,
  329. timestamp: lastTempTime,
  330. started_at: lastTempTime,
  331. date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
  332. duration: 30
  333. )
  334. var mutableIobData = iobData
  335. mutableIobData[0].lastTemp = lastTemp
  336. let result = try DeterminationGenerator.determineBasal(
  337. profile: profile,
  338. preferences: preferences,
  339. currentTemp: currentTemp,
  340. iobData: mutableIobData,
  341. mealData: mealData,
  342. autosensData: autosensData,
  343. reservoirData: reservoirData,
  344. glucoseStatus: glucoseStatus,
  345. trioCustomOrefVariables: trioCustomOrefVariables,
  346. currentTime: currentTime
  347. )
  348. #expect(result?.rate == 0)
  349. #expect(result?.duration == 0)
  350. // Note: In swift we use a different reason then JS
  351. #expect(
  352. result?
  353. .reason ==
  354. "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
  355. )
  356. }
  357. // Test 7 from JS
  358. @Test("should cancel temp if lastTemp from pumphistory ended long ago") func cancelTempOldLastTemp() throws {
  359. let (
  360. profile,
  361. preferences,
  362. _,
  363. iobData,
  364. mealData,
  365. autosensData,
  366. reservoirData,
  367. glucoseStatus,
  368. trioCustomOrefVariables,
  369. currentTime
  370. ) = createDefaultInputs()
  371. let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
  372. let lastTempTime = currentTime.addingTimeInterval(-40 * 60)
  373. let lastTemp = IobResult.LastTemp(
  374. rate: 1.5,
  375. timestamp: lastTempTime,
  376. started_at: lastTempTime,
  377. date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
  378. duration: 30
  379. )
  380. var mutableIobData = iobData
  381. mutableIobData[0].lastTemp = lastTemp
  382. let result = try DeterminationGenerator.determineBasal(
  383. profile: profile,
  384. preferences: preferences,
  385. currentTemp: currentTemp,
  386. iobData: mutableIobData,
  387. mealData: mealData,
  388. autosensData: autosensData,
  389. reservoirData: reservoirData,
  390. glucoseStatus: glucoseStatus,
  391. trioCustomOrefVariables: trioCustomOrefVariables,
  392. currentTime: currentTime
  393. )
  394. #expect(result?.rate == 0)
  395. #expect(result?.duration == 0)
  396. // Note: In swift we use a different reason then JS
  397. #expect(
  398. result?
  399. .reason ==
  400. "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
  401. )
  402. }
  403. // Test 8 from JS
  404. @Test("should throw error if eventualBG cannot be calculated") func eventualBGNaN() throws {
  405. var (
  406. profile,
  407. preferences,
  408. currentTemp,
  409. iobData,
  410. mealData,
  411. autosensData,
  412. reservoirData,
  413. glucoseStatus,
  414. trioCustomOrefVariables,
  415. currentTime
  416. ) = createDefaultInputs()
  417. profile.sens = .nan
  418. #expect(throws: DeterminationError.eventualGlucoseCalculationError(sensitivity: .nan, deviation: .nan)) {
  419. _ = try DeterminationGenerator.determineBasal(
  420. profile: profile,
  421. preferences: preferences,
  422. currentTemp: currentTemp,
  423. iobData: iobData,
  424. mealData: mealData,
  425. autosensData: autosensData,
  426. reservoirData: reservoirData,
  427. glucoseStatus: glucoseStatus,
  428. trioCustomOrefVariables: trioCustomOrefVariables,
  429. currentTime: currentTime
  430. )
  431. }
  432. }
  433. // Test 9 from JS
  434. @Test("should low-temp if BG is below threshold") func lowGlucoseSuspend() throws {
  435. var (
  436. profile,
  437. preferences,
  438. currentTemp,
  439. iobData,
  440. mealData,
  441. autosensData,
  442. reservoirData,
  443. _,
  444. trioCustomOrefVariables,
  445. currentTime
  446. ) = createDefaultInputs()
  447. let glucoseStatus = GlucoseStatus(
  448. delta: 0,
  449. glucose: 70,
  450. noise: 1,
  451. shortAvgDelta: 0,
  452. longAvgDelta: 0.1,
  453. date: currentTime,
  454. lastCalIndex: nil,
  455. device: "test"
  456. )
  457. let result = try DeterminationGenerator.determineBasal(
  458. profile: profile,
  459. preferences: preferences,
  460. currentTemp: currentTemp,
  461. iobData: iobData,
  462. mealData: mealData,
  463. autosensData: autosensData,
  464. reservoirData: reservoirData,
  465. glucoseStatus: glucoseStatus,
  466. trioCustomOrefVariables: trioCustomOrefVariables,
  467. currentTime: currentTime
  468. )
  469. #expect(result?.rate == 0)
  470. #expect((result?.duration ?? 0) >= 30)
  471. #expect(result?.reason.contains("minGuardBG") == true)
  472. }
  473. // Test 10 from JS
  474. @Test("should cancel temp before the hour if not doing SMB") func skipNeutralTemp() throws {
  475. var (
  476. profile,
  477. preferences,
  478. currentTemp,
  479. iobData,
  480. mealData,
  481. autosensData,
  482. reservoirData,
  483. glucoseStatus,
  484. trioCustomOrefVariables,
  485. _
  486. ) = createDefaultInputs()
  487. profile.skipNeutralTemps = true
  488. // Create a date that is 56 minutes past the hour
  489. var components = Calendar.current.dateComponents(in: .current, from: Date())
  490. components.minute = 56
  491. let currentTime = Calendar.current.date(from: components)!
  492. let result = try DeterminationGenerator.determineBasal(
  493. profile: profile,
  494. preferences: preferences,
  495. currentTemp: currentTemp,
  496. iobData: iobData,
  497. mealData: mealData,
  498. autosensData: autosensData,
  499. reservoirData: reservoirData,
  500. glucoseStatus: glucoseStatus,
  501. trioCustomOrefVariables: trioCustomOrefVariables,
  502. currentTime: currentTime
  503. )
  504. #expect(result?.rate == 0)
  505. #expect(result?.duration == 0)
  506. #expect(result?.reason.contains("Canceling temp") == true)
  507. }
  508. }