DetermineBasalAggressiveDosingTests.swift 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("DetermineBasalAggressiveDosingTests") struct DetermineBasalAggressiveDosingTests {
  5. private func callCalculateInsulinRequired(
  6. minForecastGlucose: Decimal,
  7. eventualGlucose: Decimal,
  8. targetGlucose: Decimal,
  9. adjustedSensitivity: Decimal,
  10. maxIob: Decimal,
  11. currentIob: Decimal
  12. ) -> (insulinRequired: Decimal, determination: Determination) {
  13. let determination = Determination(
  14. id: UUID(),
  15. reason: "",
  16. units: nil,
  17. insulinReq: nil,
  18. eventualBG: nil,
  19. sensitivityRatio: nil,
  20. rate: nil,
  21. duration: nil,
  22. iob: nil,
  23. cob: nil,
  24. predictions: nil,
  25. deliverAt: nil,
  26. carbsReq: nil,
  27. temp: nil,
  28. bg: nil,
  29. reservoir: nil,
  30. isf: nil,
  31. timestamp: nil,
  32. tdd: nil,
  33. current_target: nil,
  34. minDelta: nil,
  35. expectedDelta: nil,
  36. minGuardBG: nil,
  37. minPredBG: nil,
  38. threshold: nil,
  39. carbRatio: nil,
  40. received: nil
  41. )
  42. return DosingEngine.calculateInsulinRequired(
  43. minForecastGlucose: minForecastGlucose,
  44. eventualGlucose: eventualGlucose,
  45. targetGlucose: targetGlucose,
  46. adjustedSensitivity: adjustedSensitivity,
  47. maxIob: maxIob,
  48. currentIob: currentIob,
  49. determination: determination
  50. )
  51. }
  52. @Test("should calculate insulin required based on minPredBG when it is lower") func testCalculateBasedOnMinForecast() {
  53. // minPredBG (150) < eventualBG (180)
  54. // (150 - 100) / 50 = 1.0 U
  55. let result = callCalculateInsulinRequired(
  56. minForecastGlucose: 150,
  57. eventualGlucose: 180,
  58. targetGlucose: 100,
  59. adjustedSensitivity: 50,
  60. maxIob: 5,
  61. currentIob: 0
  62. )
  63. #expect(result.insulinRequired == 1.0)
  64. #expect(result.determination.insulinReq == 1.0)
  65. }
  66. @Test("should calculate insulin required based on eventualBG when it is lower") func testCalculateBasedOnEventual() {
  67. // eventualBG (140) < minPredBG (160)
  68. // (140 - 100) / 40 = 1.0 U
  69. let result = callCalculateInsulinRequired(
  70. minForecastGlucose: 160,
  71. eventualGlucose: 140,
  72. targetGlucose: 100,
  73. adjustedSensitivity: 40,
  74. maxIob: 5,
  75. currentIob: 0
  76. )
  77. #expect(result.insulinRequired == 1.0)
  78. #expect(result.determination.insulinReq == 1.0)
  79. }
  80. @Test("should cap insulinReq at max_iob - current_iob") func testCapAtMaxIOB() {
  81. // (200 - 100) / 20 = 5.0 U required
  82. // max_iob (3) - current_iob (1) = 2.0 U available space
  83. let result = callCalculateInsulinRequired(
  84. minForecastGlucose: 200,
  85. eventualGlucose: 200,
  86. targetGlucose: 100,
  87. adjustedSensitivity: 20,
  88. maxIob: 3,
  89. currentIob: 1
  90. )
  91. #expect(result.insulinRequired == 2.0)
  92. #expect(result.determination.reason.contains("max_iob 3"))
  93. }
  94. @Test("should not cap if insulinReq is within max_iob limits") func testNoCapWithinLimits() {
  95. // (140 - 100) / 20 = 2.0 U required
  96. // max_iob (5) - current_iob (1) = 4.0 U available space
  97. let result = callCalculateInsulinRequired(
  98. minForecastGlucose: 140,
  99. eventualGlucose: 140,
  100. targetGlucose: 100,
  101. adjustedSensitivity: 20,
  102. maxIob: 5,
  103. currentIob: 1
  104. )
  105. #expect(result.insulinRequired == 2.0)
  106. #expect(!result.determination.reason.contains("max_iob"))
  107. }
  108. @Test("should handle negative IOB increasing available space") func testNegativeIOBIncreasesSpace() {
  109. // (200 - 100) / 20 = 5.0 U required
  110. // max_iob (3) - current_iob (-1) = 4.0 U available space
  111. let result = callCalculateInsulinRequired(
  112. minForecastGlucose: 200,
  113. eventualGlucose: 200,
  114. targetGlucose: 100,
  115. adjustedSensitivity: 20,
  116. maxIob: 3,
  117. currentIob: -1
  118. )
  119. #expect(result.insulinRequired == 4.0)
  120. #expect(result.determination.reason.contains("max_iob 3"))
  121. }
  122. @Test("should handle negative insulinReq correctly") func testNegativeInsulinReq() {
  123. // (90 - 100) / 50 = -0.2 U
  124. let result = callCalculateInsulinRequired(
  125. minForecastGlucose: 90,
  126. eventualGlucose: 95,
  127. targetGlucose: 100,
  128. adjustedSensitivity: 50,
  129. maxIob: 5,
  130. currentIob: 0
  131. )
  132. #expect(result.insulinRequired == -0.2)
  133. }
  134. @Test("should round calculations to 2 decimal places") func testRounding() {
  135. // (133 - 100) / 30 = 1.1
  136. let result = callCalculateInsulinRequired(
  137. minForecastGlucose: 133,
  138. eventualGlucose: 133,
  139. targetGlucose: 100,
  140. adjustedSensitivity: 30,
  141. maxIob: 5,
  142. currentIob: 0
  143. )
  144. #expect(result.insulinRequired == 1.1)
  145. }
  146. private func callDetermineSMBDelivery(
  147. insulinRequired: Decimal,
  148. microBolusAllowed: Bool = true,
  149. smbIsEnabled: Bool = true,
  150. currentGlucose: Decimal = 120,
  151. threshold: Decimal = 60,
  152. profile: Profile,
  153. mealData: ComputedCarbs = ComputedCarbs(
  154. carbs: 0,
  155. mealCOB: 0,
  156. currentDeviation: 0,
  157. maxDeviation: 0,
  158. minDeviation: 0,
  159. slopeFromMaxDeviation: 0,
  160. slopeFromMinDeviation: 0,
  161. allDeviations: [],
  162. lastCarbTime: 0
  163. ),
  164. iobData: [IobResult],
  165. overrideFactor: Decimal = 1.0,
  166. adjustedCarbRatio: Decimal = 10,
  167. basal: Decimal = 1.0,
  168. naiveEventualGlucose: Decimal = 120,
  169. minIOBForecastedGlucose: Decimal = 120
  170. ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
  171. let determination = Determination(
  172. id: UUID(),
  173. reason: "",
  174. units: nil,
  175. insulinReq: nil,
  176. eventualBG: nil,
  177. sensitivityRatio: nil,
  178. rate: nil,
  179. duration: nil,
  180. iob: nil,
  181. cob: nil,
  182. predictions: nil,
  183. deliverAt: nil,
  184. carbsReq: nil,
  185. temp: nil,
  186. bg: nil,
  187. reservoir: nil,
  188. isf: nil,
  189. timestamp: nil,
  190. tdd: nil,
  191. current_target: nil,
  192. minDelta: nil,
  193. expectedDelta: nil,
  194. minGuardBG: nil,
  195. minPredBG: nil,
  196. threshold: nil,
  197. carbRatio: nil,
  198. received: nil
  199. )
  200. return try DosingEngine.determineSMBDelivery(
  201. insulinRequired: insulinRequired,
  202. microBolusAllowed: microBolusAllowed,
  203. smbIsEnabled: smbIsEnabled,
  204. currentGlucose: currentGlucose,
  205. threshold: threshold,
  206. profile: profile,
  207. mealData: mealData,
  208. iobData: iobData,
  209. currentTime: Date(),
  210. targetGlucose: profile.targetBg ?? 100,
  211. naiveEventualGlucose: naiveEventualGlucose,
  212. minIOBForecastedGlucose: minIOBForecastedGlucose,
  213. adjustedSensitivity: profile.sens ?? 40,
  214. overrideFactor: overrideFactor,
  215. adjustedCarbRatio: adjustedCarbRatio,
  216. basal: basal,
  217. determination: determination
  218. )
  219. }
  220. @Test("should calculate correct microbolus with rounding") func testMicroBolusRounding() throws {
  221. var profile = Profile()
  222. profile.currentBasal = 1.0
  223. profile.maxSMBBasalMinutes = 30
  224. profile.smbDeliveryRatio = 0.5
  225. profile.bolusIncrement = 0.1
  226. profile.sens = 40
  227. profile.targetBg = 100
  228. let now = Date()
  229. let lastBolusTime = UInt64(now.addingTimeInterval(-600).timeIntervalSince1970 * 1000) // 10 minutes ago
  230. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  231. iob: 0,
  232. activity: 0,
  233. basaliob: 0,
  234. bolusiob: 0,
  235. netbasalinsulin: 0,
  236. bolusinsulin: 0,
  237. time: Date()
  238. )
  239. let iobData = [IobResult(
  240. iob: 0,
  241. activity: 0,
  242. basaliob: 0,
  243. bolusiob: 0,
  244. netbasalinsulin: 0,
  245. bolusinsulin: 0,
  246. time: Date(),
  247. iobWithZeroTemp: dummyIobWithZeroTemp,
  248. lastBolusTime: lastBolusTime,
  249. lastTemp: nil
  250. )]
  251. // insulinReq = 1.55
  252. // maxBolus = 1.0 * 30/60 = 0.5
  253. // smb = min(1.55 * 0.5, 0.5) = min(0.775, 0.5) = 0.5
  254. // 0.5 is already rounded.
  255. // Let's try a case where maxBolus is higher.
  256. profile.maxSMBBasalMinutes = 60
  257. // maxBolus = 1.0 * 60/60 = 1.0
  258. // smb = min(1.55 * 0.5, 1.0) = 0.775
  259. // rounded down to 0.1 increment: 0.7
  260. let result = try callDetermineSMBDelivery(
  261. insulinRequired: 1.55,
  262. profile: profile,
  263. iobData: iobData
  264. )
  265. #expect(result.determination.units == 0.7)
  266. }
  267. @Test("should apply override factor to maxBolus") func testOverrideFactorMaxBolus() throws {
  268. var profile = Profile()
  269. profile.currentBasal = 1.0
  270. profile.maxSMBBasalMinutes = 30
  271. profile.smbDeliveryRatio = 0.5
  272. profile.bolusIncrement = 0.1
  273. profile.sens = 40
  274. profile.targetBg = 100
  275. let now = Date()
  276. let lastBolusTime = UInt64(now.addingTimeInterval(-600).timeIntervalSince1970 * 1000) // 10 minutes ago
  277. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  278. iob: 0,
  279. activity: 0,
  280. basaliob: 0,
  281. bolusiob: 0,
  282. netbasalinsulin: 0,
  283. bolusinsulin: 0,
  284. time: Date()
  285. )
  286. let iobData = [IobResult(
  287. iob: 0,
  288. activity: 0,
  289. basaliob: 0,
  290. bolusiob: 0,
  291. netbasalinsulin: 0,
  292. bolusinsulin: 0,
  293. time: Date(),
  294. iobWithZeroTemp: dummyIobWithZeroTemp,
  295. lastBolusTime: lastBolusTime,
  296. lastTemp: nil
  297. )]
  298. // Override factor 2.0 (200%)
  299. // maxBolus = 1.0 * 2.0 * 30/60 = 1.0
  300. // insulinReq = 3.0
  301. // smb = min(3.0 * 0.5, 1.0) = min(1.5, 1.0) = 1.0
  302. let result = try callDetermineSMBDelivery(
  303. insulinRequired: 3.0,
  304. profile: profile,
  305. iobData: iobData,
  306. overrideFactor: 2.0
  307. )
  308. #expect(result.determination.units == 1.0)
  309. }
  310. @Test("should use UAM max minutes when appropriate") func testUAMMaxMinutes() throws {
  311. var profile = Profile()
  312. profile.currentBasal = 1.0
  313. profile.maxSMBBasalMinutes = 30 // 0.5U
  314. profile.maxUAMSMBBasalMinutes = 60 // 1.0U
  315. profile.smbDeliveryRatio = 0.5
  316. profile.bolusIncrement = 0.1
  317. profile.sens = 40
  318. profile.targetBg = 100
  319. let now = Date()
  320. let lastBolusTime = UInt64(now.addingTimeInterval(-600).timeIntervalSince1970 * 1000) // 10 minutes ago
  321. // IOB > mealInsulinReq
  322. // mealCOB = 10, CR = 10 => mealInsulinReq = 1.0
  323. // iob = 1.5
  324. let mealData = ComputedCarbs(
  325. carbs: 0,
  326. mealCOB: 10,
  327. currentDeviation: 0,
  328. maxDeviation: 0,
  329. minDeviation: 0,
  330. slopeFromMaxDeviation: 0,
  331. slopeFromMinDeviation: 0,
  332. allDeviations: [],
  333. lastCarbTime: 0
  334. )
  335. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  336. iob: 0,
  337. activity: 0,
  338. basaliob: 0,
  339. bolusiob: 0,
  340. netbasalinsulin: 0,
  341. bolusinsulin: 0,
  342. time: Date()
  343. )
  344. let iobData = [IobResult(
  345. iob: 1.5,
  346. activity: 0,
  347. basaliob: 0,
  348. bolusiob: 0,
  349. netbasalinsulin: 0,
  350. bolusinsulin: 0,
  351. time: Date(),
  352. iobWithZeroTemp: dummyIobWithZeroTemp,
  353. lastBolusTime: lastBolusTime,
  354. lastTemp: nil
  355. )]
  356. // insulinReq = 3.0
  357. // smb = min(3.0 * 0.5, 1.0) = 1.0 (uses UAM limit)
  358. let result = try callDetermineSMBDelivery(
  359. insulinRequired: 3.0,
  360. profile: profile,
  361. mealData: mealData,
  362. iobData: iobData
  363. )
  364. #expect(result.determination.units == 1.0)
  365. }
  366. @Test("should not bolus if within SMB interval") func testSMBInterval() throws {
  367. var profile = Profile()
  368. profile.currentBasal = 1.0
  369. profile.maxSMBBasalMinutes = 30
  370. profile.smbDeliveryRatio = 0.5
  371. profile.bolusIncrement = 0.1
  372. profile.smbInterval = 3
  373. profile.sens = 40
  374. profile.targetBg = 100
  375. // Last bolus 1 minute ago
  376. let now = Date()
  377. let lastBolusTime = UInt64(now.addingTimeInterval(-60).timeIntervalSince1970 * 1000)
  378. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  379. iob: 0,
  380. activity: 0,
  381. basaliob: 0,
  382. bolusiob: 0,
  383. netbasalinsulin: 0,
  384. bolusinsulin: 0,
  385. time: Date()
  386. )
  387. let iobData = [IobResult(
  388. iob: 0,
  389. activity: 0,
  390. basaliob: 0,
  391. bolusiob: 0,
  392. netbasalinsulin: 0,
  393. bolusinsulin: 0,
  394. time: now,
  395. iobWithZeroTemp: dummyIobWithZeroTemp,
  396. lastBolusTime: lastBolusTime,
  397. lastTemp: nil
  398. )]
  399. // Setup conditions where temp basal is required so it returns true, but NO units
  400. // worstCase > 0 => low pred
  401. // (100 - (90+90)/2) / 40 = 0.25
  402. // duration = 60 * 0.25 / 1 = 15m
  403. // 15m -> <30m -> sets smbLowTempReq -> returns true
  404. let result = try callDetermineSMBDelivery(
  405. insulinRequired: 1.0,
  406. profile: profile,
  407. iobData: iobData,
  408. naiveEventualGlucose: 90,
  409. minIOBForecastedGlucose: 90
  410. )
  411. #expect(result.shouldSetTempBasal == true)
  412. #expect(result.determination.units == nil)
  413. #expect(result.determination.reason.contains("Waiting"))
  414. }
  415. @Test("should return false if SMB conditions not met") func testGuardConditions() throws {
  416. var profile = Profile()
  417. profile.currentBasal = 1.0
  418. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  419. iob: 0,
  420. activity: 0,
  421. basaliob: 0,
  422. bolusiob: 0,
  423. netbasalinsulin: 0,
  424. bolusinsulin: 0,
  425. time: Date()
  426. )
  427. let iobData = [IobResult(
  428. iob: 0,
  429. activity: 0,
  430. basaliob: 0,
  431. bolusiob: 0,
  432. netbasalinsulin: 0,
  433. bolusinsulin: 0,
  434. time: Date(),
  435. iobWithZeroTemp: dummyIobWithZeroTemp,
  436. lastBolusTime: nil,
  437. lastTemp: nil
  438. )]
  439. // bg (100) < threshold (110)
  440. let result = try callDetermineSMBDelivery(
  441. insulinRequired: 1.0,
  442. currentGlucose: 100,
  443. threshold: 110,
  444. profile: profile,
  445. iobData: iobData
  446. )
  447. #expect(result.shouldSetTempBasal == false)
  448. }
  449. private func callDetermineHighTempBasal(
  450. insulinRequired: Decimal,
  451. basal: Decimal,
  452. profile: Profile,
  453. currentTemp: TempBasal
  454. ) throws -> Determination {
  455. let determination = Determination(
  456. id: UUID(),
  457. reason: "",
  458. units: nil,
  459. insulinReq: nil,
  460. eventualBG: nil,
  461. sensitivityRatio: nil,
  462. rate: nil,
  463. duration: nil,
  464. iob: nil,
  465. cob: nil,
  466. predictions: nil,
  467. deliverAt: nil,
  468. carbsReq: nil,
  469. temp: nil,
  470. bg: nil,
  471. reservoir: nil,
  472. isf: nil,
  473. timestamp: nil,
  474. tdd: nil,
  475. current_target: nil,
  476. minDelta: nil,
  477. expectedDelta: nil,
  478. minGuardBG: nil,
  479. minPredBG: nil,
  480. threshold: nil,
  481. carbRatio: nil,
  482. received: nil
  483. )
  484. return try DosingEngine.determineHighTempBasal(
  485. insulinRequired: insulinRequired,
  486. basal: basal,
  487. profile: profile,
  488. currentTemp: currentTemp,
  489. determination: determination
  490. )
  491. }
  492. @Test("should set high temp if no temp is running") func testSetHighTempNoTemp() throws {
  493. var profile = Profile()
  494. profile.maxBasal = 5.0
  495. profile.maxDailyBasal = 5.0
  496. profile.currentBasal = 1.0 // Unused by logic but good for completeness
  497. // insulinReq = 1.0. basal = 1.0. rate = 1.0 + 2*1.0 = 3.0.
  498. let result = try callDetermineHighTempBasal(
  499. insulinRequired: 1.0,
  500. basal: 1.0,
  501. profile: profile,
  502. currentTemp: TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  503. )
  504. #expect(result.rate == 3.0)
  505. #expect(result.duration == 30)
  506. #expect(result.reason.contains("no temp, setting 3U/hr"))
  507. }
  508. @Test("should cap rate at maxSafeBasal") func testCapAtMaxSafeBasal() throws {
  509. var profile = Profile()
  510. profile.maxBasal = 2.0 // Restrict max basal
  511. profile.maxDailyBasal = 2.0
  512. profile.currentBasalSafetyMultiplier = 4
  513. profile.maxDailySafetyMultiplier = 3
  514. profile.currentBasal = 1.0
  515. // insulinReq = 1.0. basal = 1.0. rate = 3.0. Max = 2.0.
  516. let result = try callDetermineHighTempBasal(
  517. insulinRequired: 1.0,
  518. basal: 1.0,
  519. profile: profile,
  520. currentTemp: TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  521. )
  522. #expect(result.rate == 2.0)
  523. #expect(result.reason.contains("adj. req. rate: 3"))
  524. #expect(result.reason.contains("maxSafeBasal: 2"))
  525. }
  526. @Test("should reduce temp if current temp delivers >2x required insulin") func testReduceTempIfScheduledTooHigh() throws {
  527. var profile = Profile()
  528. profile.maxBasal = 5.0
  529. profile.maxDailyBasal = 5.0
  530. profile.currentBasal = 1.0
  531. // insulinReq = 0.5. 2x = 1.0 U.
  532. // basal = 1.0. rate = 1.0 + 1.0 = 2.0.
  533. // Current temp: rate 4.0, duration 30m.
  534. // insulinScheduled = 30 * (4.0 - 1.0) / 60 = 1.5 U.
  535. let result = try callDetermineHighTempBasal(
  536. insulinRequired: 0.5,
  537. basal: 1.0,
  538. profile: profile,
  539. currentTemp: TempBasal(duration: 30, rate: 4.0, temp: .absolute, timestamp: Date())
  540. )
  541. #expect(result.rate == 2.0)
  542. #expect(result.reason.contains("> 2 * insulinReq"))
  543. }
  544. @Test("should do nothing if current temp is sufficient") func testDoNothingIfSufficient() throws {
  545. var profile = Profile()
  546. profile.maxBasal = 5.0
  547. profile.maxDailyBasal = 5.0
  548. profile.currentBasal = 1.0
  549. // insulinReq = 1.0. rate = 3.0.
  550. // Current temp: rate 3.0, duration 30m.
  551. let result = try callDetermineHighTempBasal(
  552. insulinRequired: 1.0,
  553. basal: 1.0,
  554. profile: profile,
  555. currentTemp: TempBasal(duration: 30, rate: 3.0, temp: .absolute, timestamp: Date())
  556. )
  557. // Should return determination without setting rate/duration (nil implies unchanged in this context check?)
  558. // Wait, determineHighTempBasal returns a Determination. If it calls setTempBasal, rate/duration are set.
  559. // If it falls through, it returns 'determination' (which has nil rate/duration).
  560. #expect(result.rate == nil)
  561. #expect(result.duration == nil)
  562. #expect(result.reason.contains("temp 3 >~ req 3U/hr"))
  563. }
  564. @Test("should set new temp if current temp is insufficient") func testSetNewTempIfInsufficient() throws {
  565. var profile = Profile()
  566. profile.maxBasal = 5.0
  567. profile.maxDailyBasal = 5.0
  568. profile.currentBasal = 1.0
  569. // insulinReq = 1.0. rate = 3.0.
  570. // Current temp: rate 2.0.
  571. let result = try callDetermineHighTempBasal(
  572. insulinRequired: 1.0,
  573. basal: 1.0,
  574. profile: profile,
  575. currentTemp: TempBasal(duration: 30, rate: 2.0, temp: .absolute, timestamp: Date())
  576. )
  577. #expect(result.rate == 3.0)
  578. #expect(result.duration == 30)
  579. #expect(result.reason.contains("temp 2<3U/hr"))
  580. }
  581. @Test("should set 30m zero temp if durationReq is between 30 and 45") func testSet30mZeroTemp() throws {
  582. var profile = Profile()
  583. profile.currentBasal = 1.0
  584. profile.maxSMBBasalMinutes = 30
  585. profile.smbDeliveryRatio = 0.5
  586. profile.bolusIncrement = 0.1
  587. profile.sens = 50
  588. profile.targetBg = 100
  589. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  590. iob: 0,
  591. activity: 0,
  592. basaliob: 0,
  593. bolusiob: 0,
  594. netbasalinsulin: 0,
  595. bolusinsulin: 0,
  596. time: Date()
  597. )
  598. let iobData = [IobResult(
  599. iob: 0,
  600. activity: 0,
  601. basaliob: 0,
  602. bolusiob: 0,
  603. netbasalinsulin: 0,
  604. bolusinsulin: 0,
  605. time: Date(),
  606. iobWithZeroTemp: dummyIobWithZeroTemp,
  607. lastBolusTime: nil,
  608. lastTemp: nil
  609. )]
  610. // worstCaseInsulinReq needs to result in durationReq ~ 35
  611. // duration = 60 * worst / basal => 35 = 60 * worst / 1.0 => worst = 0.583
  612. // worst = (100 - avg)/50 => avg = 70.85
  613. let result = try callDetermineSMBDelivery(
  614. insulinRequired: 1.0,
  615. profile: profile,
  616. iobData: iobData,
  617. naiveEventualGlucose: 70.85,
  618. minIOBForecastedGlucose: 70.85
  619. )
  620. #expect(result.shouldSetTempBasal == true)
  621. #expect(result.determination.rate == 0)
  622. #expect(result.determination.duration == 30)
  623. }
  624. @Test("should set 60m zero temp if durationReq is > 45") func testSet60mZeroTemp() throws {
  625. var profile = Profile()
  626. profile.currentBasal = 1.0
  627. profile.maxSMBBasalMinutes = 30
  628. profile.smbDeliveryRatio = 0.5
  629. profile.bolusIncrement = 0.1
  630. profile.sens = 50
  631. profile.targetBg = 100
  632. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  633. iob: 0,
  634. activity: 0,
  635. basaliob: 0,
  636. bolusiob: 0,
  637. netbasalinsulin: 0,
  638. bolusinsulin: 0,
  639. time: Date()
  640. )
  641. let iobData = [IobResult(
  642. iob: 0,
  643. activity: 0,
  644. basaliob: 0,
  645. bolusiob: 0,
  646. netbasalinsulin: 0,
  647. bolusinsulin: 0,
  648. time: Date(),
  649. iobWithZeroTemp: dummyIobWithZeroTemp,
  650. lastBolusTime: nil,
  651. lastTemp: nil
  652. )]
  653. // worstCaseInsulinReq needs to result in durationReq ~ 50
  654. // 50 = 60 * worst / 1.0 => worst = 0.833
  655. // worst = (100 - avg)/50 => avg = 58.35
  656. let result = try callDetermineSMBDelivery(
  657. insulinRequired: 1.0,
  658. profile: profile,
  659. iobData: iobData,
  660. naiveEventualGlucose: 58.35,
  661. minIOBForecastedGlucose: 58.35
  662. )
  663. #expect(result.shouldSetTempBasal == true)
  664. #expect(result.determination.rate == 0)
  665. #expect(result.determination.duration == 60)
  666. }
  667. @Test("should cap zero temp duration at 60m") func testCapZeroTempAt60m() throws {
  668. var profile = Profile()
  669. profile.currentBasal = 1.0
  670. profile.maxSMBBasalMinutes = 30
  671. profile.smbDeliveryRatio = 0.5
  672. profile.bolusIncrement = 0.1
  673. profile.sens = 50
  674. profile.targetBg = 100
  675. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  676. iob: 0,
  677. activity: 0,
  678. basaliob: 0,
  679. bolusiob: 0,
  680. netbasalinsulin: 0,
  681. bolusinsulin: 0,
  682. time: Date()
  683. )
  684. let iobData = [IobResult(
  685. iob: 0,
  686. activity: 0,
  687. basaliob: 0,
  688. bolusiob: 0,
  689. netbasalinsulin: 0,
  690. bolusinsulin: 0,
  691. time: Date(),
  692. iobWithZeroTemp: dummyIobWithZeroTemp,
  693. lastBolusTime: nil,
  694. lastTemp: nil
  695. )]
  696. // worstCaseInsulinReq needs to result in durationReq > 75
  697. // 100 = 60 * worst / 1.0 => worst = 1.66
  698. // worst = (100 - avg)/50 => avg = 17
  699. let result = try callDetermineSMBDelivery(
  700. insulinRequired: 1.0,
  701. profile: profile,
  702. iobData: iobData,
  703. naiveEventualGlucose: 17,
  704. minIOBForecastedGlucose: 17
  705. )
  706. #expect(result.shouldSetTempBasal == true)
  707. #expect(result.determination.rate == 0)
  708. #expect(result.determination.duration == 60)
  709. }
  710. @Test("should set low temp if durationReq < 30") func testSetLowTemp() throws {
  711. var profile = Profile()
  712. profile.currentBasal = 1.0
  713. profile.maxSMBBasalMinutes = 30
  714. profile.smbDeliveryRatio = 0.5
  715. profile.bolusIncrement = 0.1
  716. profile.sens = 50
  717. profile.targetBg = 100
  718. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  719. iob: 0,
  720. activity: 0,
  721. basaliob: 0,
  722. bolusiob: 0,
  723. netbasalinsulin: 0,
  724. bolusinsulin: 0,
  725. time: Date()
  726. )
  727. let iobData = [IobResult(
  728. iob: 0,
  729. activity: 0,
  730. basaliob: 0,
  731. bolusiob: 0,
  732. netbasalinsulin: 0,
  733. bolusinsulin: 0,
  734. time: Date(),
  735. iobWithZeroTemp: dummyIobWithZeroTemp,
  736. lastBolusTime: nil,
  737. lastTemp: nil
  738. )]
  739. // worstCaseInsulinReq needs to result in durationReq = 15
  740. // 15 = 60 * worst / 1.0 => worst = 0.25
  741. // worst = (100 - avg)/50 => avg = 87.5
  742. let result = try callDetermineSMBDelivery(
  743. insulinRequired: 1.0,
  744. profile: profile,
  745. iobData: iobData,
  746. basal: 1.0,
  747. naiveEventualGlucose: 87.5,
  748. minIOBForecastedGlucose: 87.5
  749. )
  750. // Rate = basal * 15/30 = 1.0 * 0.5 = 0.5
  751. #expect(result.shouldSetTempBasal == true)
  752. #expect(result.determination.rate == 0.5)
  753. #expect(result.determination.duration == 30)
  754. }
  755. @Test("should not set temp if insulinReq > 0 but microBolus < increment") func testNoTempIfMicroBolusTooSmall() throws {
  756. var profile = Profile()
  757. profile.currentBasal = 1.0
  758. profile.maxSMBBasalMinutes = 30
  759. profile.smbDeliveryRatio = 0.5
  760. profile.bolusIncrement = 0.1
  761. profile.sens = 50
  762. profile.targetBg = 100
  763. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  764. iob: 0,
  765. activity: 0,
  766. basaliob: 0,
  767. bolusiob: 0,
  768. netbasalinsulin: 0,
  769. bolusinsulin: 0,
  770. time: Date()
  771. )
  772. let iobData = [IobResult(
  773. iob: 0,
  774. activity: 0,
  775. basaliob: 0,
  776. bolusiob: 0,
  777. netbasalinsulin: 0,
  778. bolusinsulin: 0,
  779. time: Date(),
  780. iobWithZeroTemp: dummyIobWithZeroTemp,
  781. lastBolusTime: nil,
  782. lastTemp: nil
  783. )]
  784. // insulinReq = 0.05 (positive but small)
  785. // microBolus < 0.1
  786. // durationReq = 15m (via predictions)
  787. let result = try callDetermineSMBDelivery(
  788. insulinRequired: 0.05,
  789. profile: profile,
  790. iobData: iobData,
  791. basal: 1.0,
  792. naiveEventualGlucose: 87.5,
  793. minIOBForecastedGlucose: 87.5
  794. )
  795. #expect(result.shouldSetTempBasal == false)
  796. }
  797. @Test("should not set temp if durationReq <= 0") func testNoTempIfDurationReqNegative() throws {
  798. var profile = Profile()
  799. profile.currentBasal = 1.0
  800. profile.maxSMBBasalMinutes = 30
  801. profile.smbDeliveryRatio = 0.5
  802. profile.bolusIncrement = 0.1
  803. profile.sens = 50
  804. profile.targetBg = 100
  805. let dummyIobWithZeroTemp = IobResult.IobWithZeroTemp(
  806. iob: 0,
  807. activity: 0,
  808. basaliob: 0,
  809. bolusiob: 0,
  810. netbasalinsulin: 0,
  811. bolusinsulin: 0,
  812. time: Date()
  813. )
  814. let iobData = [IobResult(
  815. iob: 0,
  816. activity: 0,
  817. basaliob: 0,
  818. bolusiob: 0,
  819. netbasalinsulin: 0,
  820. bolusinsulin: 0,
  821. time: Date(),
  822. iobWithZeroTemp: dummyIobWithZeroTemp,
  823. lastBolusTime: nil,
  824. lastTemp: nil
  825. )]
  826. // High predictions => negative worstCase => negative durationReq
  827. // avg = 150 > target 100
  828. let result = try callDetermineSMBDelivery(
  829. insulinRequired: 1.0,
  830. profile: profile,
  831. iobData: iobData,
  832. basal: 1.0,
  833. naiveEventualGlucose: 150,
  834. minIOBForecastedGlucose: 150
  835. )
  836. #expect(result.shouldSetTempBasal == false)
  837. }
  838. }