DetermineBasalEarlyExitTests.swift 17 KB

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