DoseMathTests.swift 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492
  1. //
  2. // DoseMathTests.swift
  3. // NateradeTests
  4. //
  5. // Created by Nathan Racklyeft on 3/8/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. @testable import LoopKit
  11. extension ISO8601DateFormatter {
  12. static func localTimeDateFormatter() -> Self {
  13. let formatter = self.init()
  14. formatter.formatOptions = .withInternetDateTime
  15. formatter.formatOptions.subtract(.withTimeZone)
  16. formatter.timeZone = .current
  17. return formatter
  18. }
  19. }
  20. struct SimpleGlucoseFixtureValue: GlucoseValue {
  21. let startDate: Date
  22. let quantity: HKQuantity
  23. init(startDate: Date, quantity: HKQuantity) {
  24. self.startDate = startDate
  25. self.quantity = quantity
  26. }
  27. }
  28. class RecommendTempBasalTests: XCTestCase {
  29. fileprivate let maxBasalRate = 3.0
  30. fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 }
  31. func loadGlucoseValueFixture(_ resourceName: String) -> [SimpleGlucoseFixtureValue] {
  32. let fixture: [JSONDictionary] = loadFixture(resourceName)
  33. let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter()
  34. return fixture.map {
  35. return SimpleGlucoseFixtureValue(
  36. startDate: dateFormatter.date(from: $0["date"] as! String)!,
  37. quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double)
  38. )
  39. }
  40. }
  41. func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule {
  42. let fixture: [JSONDictionary] = loadFixture(resourceName)
  43. let items = fixture.map {
  44. return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double)
  45. }
  46. return BasalRateSchedule(dailyItems: items)!
  47. }
  48. var basalRateSchedule: BasalRateSchedule {
  49. return loadBasalRateScheduleFixture("read_selected_basal_profile")
  50. }
  51. var glucoseTargetRange: GlucoseRangeSchedule {
  52. return GlucoseRangeSchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 90, maxValue: 120))])!
  53. }
  54. var insulinSensitivitySchedule: InsulinSensitivitySchedule {
  55. return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])!
  56. }
  57. var suspendThreshold: GlucoseThreshold {
  58. return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 55)
  59. }
  60. var exponentialInsulinModel: InsulinModel = ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0, delay: 0)
  61. var walshInsulinModel: InsulinModel {
  62. return WalshInsulinModel(actionDuration: insulinActionDuration)
  63. }
  64. var insulinActionDuration: TimeInterval {
  65. return TimeInterval(hours: 4)
  66. }
  67. func testNoChange() {
  68. let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
  69. let dose = glucose.recommendedTempBasal(
  70. to: glucoseTargetRange,
  71. at: glucose.first!.startDate,
  72. suspendThreshold: suspendThreshold.quantity,
  73. sensitivity: insulinSensitivitySchedule,
  74. model: walshInsulinModel,
  75. basalRates: basalRateSchedule,
  76. maxBasalRate: maxBasalRate,
  77. lastTempBasal: nil
  78. )
  79. XCTAssertNil(dose)
  80. }
  81. func testNoChangeExponentialModel() {
  82. let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
  83. let dose = glucose.recommendedTempBasal(
  84. to: glucoseTargetRange,
  85. at: glucose.first!.startDate,
  86. suspendThreshold: suspendThreshold.quantity,
  87. sensitivity: insulinSensitivitySchedule,
  88. model: exponentialInsulinModel,
  89. basalRates: basalRateSchedule,
  90. maxBasalRate: maxBasalRate,
  91. lastTempBasal: nil
  92. )
  93. XCTAssertNil(dose)
  94. }
  95. func testNoChangeAutomaticBolusing() {
  96. let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
  97. let dose = glucose.recommendedAutomaticDose(
  98. to: glucoseTargetRange,
  99. at: glucose.first!.startDate,
  100. suspendThreshold: suspendThreshold.quantity,
  101. sensitivity: insulinSensitivitySchedule,
  102. model: walshInsulinModel,
  103. basalRates: basalRateSchedule,
  104. maxAutomaticBolus: 5,
  105. partialApplicationFactor: 0.5,
  106. lastTempBasal: nil
  107. )
  108. XCTAssertNil(dose)
  109. }
  110. func testNoChangeOverrideActive() {
  111. let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
  112. let dose = glucose.recommendedTempBasal(
  113. to: glucoseTargetRange,
  114. at: glucose.first!.startDate,
  115. suspendThreshold: suspendThreshold.quantity,
  116. sensitivity: insulinSensitivitySchedule,
  117. model: walshInsulinModel,
  118. basalRates: basalRateSchedule,
  119. maxBasalRate: maxBasalRate,
  120. lastTempBasal: nil,
  121. isBasalRateScheduleOverrideActive: true
  122. )
  123. XCTAssertEqual(0.8, dose!.unitsPerHour, accuracy: 1.0 / 40.0)
  124. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  125. }
  126. func testNoChangeOverrideActiveAutomaticBolusing() {
  127. let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
  128. let dose = glucose.recommendedAutomaticDose(
  129. to: glucoseTargetRange,
  130. at: glucose.first!.startDate,
  131. suspendThreshold: suspendThreshold.quantity,
  132. sensitivity: insulinSensitivitySchedule,
  133. model: walshInsulinModel,
  134. basalRates: basalRateSchedule,
  135. maxAutomaticBolus: 5,
  136. partialApplicationFactor: 0.5,
  137. lastTempBasal: nil,
  138. isBasalRateScheduleOverrideActive: true
  139. )
  140. XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0)
  141. XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration)
  142. XCTAssertEqual(0, dose!.bolusUnits!)
  143. }
  144. func testStartHighEndInRange() {
  145. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range")
  146. var dose = glucose.recommendedTempBasal(
  147. to: glucoseTargetRange,
  148. at: glucose.first!.startDate,
  149. suspendThreshold: suspendThreshold.quantity,
  150. sensitivity: insulinSensitivitySchedule,
  151. model: walshInsulinModel,
  152. basalRates: basalRateSchedule,
  153. maxBasalRate: maxBasalRate,
  154. lastTempBasal: nil
  155. )
  156. XCTAssertNil(dose)
  157. // Cancel existing temp basal
  158. let lastTempBasal = DoseEntry(
  159. type: .tempBasal,
  160. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  161. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  162. value: 0.125,
  163. unit: .unitsPerHour
  164. )
  165. dose = glucose.recommendedTempBasal(
  166. to: glucoseTargetRange,
  167. at: glucose.first!.startDate,
  168. suspendThreshold: suspendThreshold.quantity,
  169. sensitivity: insulinSensitivitySchedule,
  170. model: walshInsulinModel,
  171. basalRates: basalRateSchedule,
  172. maxBasalRate: maxBasalRate,
  173. lastTempBasal: lastTempBasal
  174. )
  175. XCTAssertEqual(0, dose!.unitsPerHour)
  176. XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration)
  177. }
  178. func testStartHighEndInRangeAutomaticBolus() {
  179. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range")
  180. var dose = glucose.recommendedAutomaticDose(
  181. to: glucoseTargetRange,
  182. at: glucose.first!.startDate,
  183. suspendThreshold: suspendThreshold.quantity,
  184. sensitivity: insulinSensitivitySchedule,
  185. model: walshInsulinModel,
  186. basalRates: basalRateSchedule,
  187. maxAutomaticBolus: 5,
  188. partialApplicationFactor: 0.5,
  189. lastTempBasal: nil
  190. )
  191. XCTAssertNil(dose)
  192. // Cancel existing temp basal
  193. let lastTempBasal = DoseEntry(
  194. type: .tempBasal,
  195. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  196. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  197. value: 0.125,
  198. unit: .unitsPerHour
  199. )
  200. dose = glucose.recommendedAutomaticDose(
  201. to: glucoseTargetRange,
  202. at: glucose.first!.startDate,
  203. suspendThreshold: suspendThreshold.quantity,
  204. sensitivity: insulinSensitivitySchedule,
  205. model: walshInsulinModel,
  206. basalRates: basalRateSchedule,
  207. maxAutomaticBolus: 5,
  208. partialApplicationFactor: 0.5,
  209. lastTempBasal: lastTempBasal
  210. )
  211. XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour)
  212. XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration)
  213. XCTAssertEqual(0, dose!.bolusUnits!)
  214. }
  215. func testStartHighEndInRangeAutomaticBolusWithOverride() {
  216. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range")
  217. var dose = glucose.recommendedAutomaticDose(
  218. to: glucoseTargetRange,
  219. at: glucose.first!.startDate,
  220. suspendThreshold: suspendThreshold.quantity,
  221. sensitivity: insulinSensitivitySchedule,
  222. model: walshInsulinModel,
  223. basalRates: basalRateSchedule,
  224. maxAutomaticBolus: 5,
  225. partialApplicationFactor: 0.5,
  226. lastTempBasal: nil,
  227. isBasalRateScheduleOverrideActive: true
  228. )
  229. XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0)
  230. XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration)
  231. // Continue existing temp basal
  232. let lastTempBasal = DoseEntry(
  233. type: .tempBasal,
  234. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  235. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  236. value: 0.8,
  237. unit: .unitsPerHour
  238. )
  239. dose = glucose.recommendedAutomaticDose(
  240. to: glucoseTargetRange,
  241. at: glucose.first!.startDate,
  242. suspendThreshold: suspendThreshold.quantity,
  243. sensitivity: insulinSensitivitySchedule,
  244. model: walshInsulinModel,
  245. basalRates: basalRateSchedule,
  246. maxAutomaticBolus: 5,
  247. partialApplicationFactor: 0.5,
  248. lastTempBasal: lastTempBasal,
  249. isBasalRateScheduleOverrideActive: true
  250. )
  251. XCTAssertNil(dose)
  252. }
  253. func testStartLowEndInRange() {
  254. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range")
  255. var dose = glucose.recommendedTempBasal(
  256. to: glucoseTargetRange,
  257. at: glucose.first!.startDate,
  258. suspendThreshold: suspendThreshold.quantity,
  259. sensitivity: insulinSensitivitySchedule,
  260. model: walshInsulinModel,
  261. basalRates: basalRateSchedule,
  262. maxBasalRate: maxBasalRate,
  263. lastTempBasal: nil
  264. )
  265. XCTAssertNil(dose)
  266. let lastTempBasal = DoseEntry(
  267. type: .tempBasal,
  268. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  269. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  270. value: 1.225,
  271. unit: .unitsPerHour
  272. )
  273. dose = glucose.recommendedTempBasal(
  274. to: glucoseTargetRange,
  275. at: glucose.first!.startDate,
  276. suspendThreshold: suspendThreshold.quantity,
  277. sensitivity: insulinSensitivitySchedule,
  278. model: walshInsulinModel,
  279. basalRates: basalRateSchedule,
  280. maxBasalRate: maxBasalRate,
  281. lastTempBasal: lastTempBasal
  282. )
  283. XCTAssertEqual(0, dose!.unitsPerHour)
  284. XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration)
  285. }
  286. func testStartLowEndInRangeExponentialModel() {
  287. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range")
  288. var dose = glucose.recommendedTempBasal(
  289. to: glucoseTargetRange,
  290. at: glucose.first!.startDate,
  291. suspendThreshold: suspendThreshold.quantity,
  292. sensitivity: insulinSensitivitySchedule,
  293. model: exponentialInsulinModel,
  294. basalRates: basalRateSchedule,
  295. maxBasalRate: maxBasalRate,
  296. lastTempBasal: nil
  297. )
  298. XCTAssertNil(dose)
  299. let lastTempBasal = DoseEntry(
  300. type: .tempBasal,
  301. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  302. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  303. value: 1.225,
  304. unit: .unitsPerHour
  305. )
  306. dose = glucose.recommendedTempBasal(
  307. to: glucoseTargetRange,
  308. at: glucose.first!.startDate,
  309. suspendThreshold: suspendThreshold.quantity,
  310. sensitivity: insulinSensitivitySchedule,
  311. model: walshInsulinModel,
  312. basalRates: basalRateSchedule,
  313. maxBasalRate: maxBasalRate,
  314. lastTempBasal: lastTempBasal
  315. )
  316. XCTAssertEqual(0, dose!.unitsPerHour)
  317. XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration)
  318. }
  319. func testStartLowEndInRangeAutomaticBolus() {
  320. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range")
  321. var dose = glucose.recommendedAutomaticDose(
  322. to: glucoseTargetRange,
  323. at: glucose.first!.startDate,
  324. suspendThreshold: suspendThreshold.quantity,
  325. sensitivity: insulinSensitivitySchedule,
  326. model: walshInsulinModel,
  327. basalRates: basalRateSchedule,
  328. maxAutomaticBolus: 5,
  329. partialApplicationFactor: 0.5,
  330. lastTempBasal: nil
  331. )
  332. XCTAssertNil(dose)
  333. let lastTempBasal = DoseEntry(
  334. type: .tempBasal,
  335. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  336. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  337. value: 1.225,
  338. unit: .unitsPerHour
  339. )
  340. dose = glucose.recommendedAutomaticDose(
  341. to: glucoseTargetRange,
  342. at: glucose.first!.startDate,
  343. suspendThreshold: suspendThreshold.quantity,
  344. sensitivity: insulinSensitivitySchedule,
  345. model: walshInsulinModel,
  346. basalRates: basalRateSchedule,
  347. maxAutomaticBolus: 5,
  348. partialApplicationFactor: 0.5,
  349. lastTempBasal: lastTempBasal
  350. )
  351. XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour)
  352. XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration)
  353. XCTAssertEqual(0, dose!.bolusUnits!)
  354. }
  355. func testCorrectLowAtMin() {
  356. let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min")
  357. // Cancel existing dose
  358. let lastTempBasal = DoseEntry(
  359. type: .tempBasal,
  360. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -21)),
  361. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 9)),
  362. value: 0.125,
  363. unit: .unitsPerHour
  364. )
  365. var dose = glucose.recommendedTempBasal(
  366. to: glucoseTargetRange,
  367. at: glucose.first!.startDate,
  368. suspendThreshold: suspendThreshold.quantity,
  369. sensitivity: insulinSensitivitySchedule,
  370. model: walshInsulinModel,
  371. basalRates: basalRateSchedule,
  372. maxBasalRate: maxBasalRate,
  373. lastTempBasal: lastTempBasal
  374. )
  375. XCTAssertEqual(0, dose!.unitsPerHour)
  376. XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration)
  377. dose = glucose.recommendedTempBasal(
  378. to: glucoseTargetRange,
  379. at: glucose.first!.startDate,
  380. suspendThreshold: suspendThreshold.quantity,
  381. sensitivity: insulinSensitivitySchedule,
  382. model: walshInsulinModel,
  383. basalRates: basalRateSchedule,
  384. maxBasalRate: maxBasalRate,
  385. lastTempBasal: nil
  386. )
  387. XCTAssertNil(dose)
  388. }
  389. func testCorrectLowAtMinAutomaticBolus() {
  390. let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min")
  391. // Cancel existing dose
  392. let lastTempBasal = DoseEntry(
  393. type: .tempBasal,
  394. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -21)),
  395. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 9)),
  396. value: 0.125,
  397. unit: .unitsPerHour
  398. )
  399. var dose = glucose.recommendedAutomaticDose(
  400. to: glucoseTargetRange,
  401. at: glucose.first!.startDate,
  402. suspendThreshold: suspendThreshold.quantity,
  403. sensitivity: insulinSensitivitySchedule,
  404. model: walshInsulinModel,
  405. basalRates: basalRateSchedule,
  406. maxAutomaticBolus: 5,
  407. partialApplicationFactor: 0.5,
  408. lastTempBasal: lastTempBasal
  409. )
  410. XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour)
  411. XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration)
  412. XCTAssertEqual(0, dose!.bolusUnits!)
  413. dose = glucose.recommendedAutomaticDose(
  414. to: glucoseTargetRange,
  415. at: glucose.first!.startDate,
  416. suspendThreshold: suspendThreshold.quantity,
  417. sensitivity: insulinSensitivitySchedule,
  418. model: walshInsulinModel,
  419. basalRates: basalRateSchedule,
  420. maxAutomaticBolus: 5,
  421. partialApplicationFactor: 0.5,
  422. lastTempBasal: nil
  423. )
  424. XCTAssertNil(dose)
  425. }
  426. func testStartHighEndLow() {
  427. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low")
  428. let dose = glucose.recommendedTempBasal(
  429. to: glucoseTargetRange,
  430. at: glucose.first!.startDate,
  431. suspendThreshold: suspendThreshold.quantity,
  432. sensitivity: insulinSensitivitySchedule,
  433. model: walshInsulinModel,
  434. basalRates: basalRateSchedule,
  435. maxBasalRate: maxBasalRate,
  436. lastTempBasal: nil
  437. )
  438. XCTAssertEqual(0, dose!.unitsPerHour)
  439. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  440. }
  441. func testStartHighEndLowAutomaticBolus() {
  442. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low")
  443. let dose = glucose.recommendedAutomaticDose(
  444. to: glucoseTargetRange,
  445. at: glucose.first!.startDate,
  446. suspendThreshold: suspendThreshold.quantity,
  447. sensitivity: insulinSensitivitySchedule,
  448. model: walshInsulinModel,
  449. basalRates: basalRateSchedule,
  450. maxAutomaticBolus: 5,
  451. partialApplicationFactor: 0.5,
  452. lastTempBasal: nil
  453. )
  454. XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour)
  455. XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration)
  456. XCTAssertEqual(0, dose!.bolusUnits!)
  457. }
  458. func testStartHighEndLowExponentialModel() {
  459. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low")
  460. let dose = glucose.recommendedTempBasal(
  461. to: glucoseTargetRange,
  462. at: glucose.first!.startDate,
  463. suspendThreshold: suspendThreshold.quantity,
  464. sensitivity: insulinSensitivitySchedule,
  465. model: exponentialInsulinModel,
  466. basalRates: basalRateSchedule,
  467. maxBasalRate: maxBasalRate,
  468. lastTempBasal: nil
  469. )
  470. XCTAssertEqual(0, dose!.unitsPerHour)
  471. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  472. }
  473. func testStartLowEndHigh() {
  474. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  475. var dose = glucose.recommendedTempBasal(
  476. to: glucoseTargetRange,
  477. at: glucose.first!.startDate,
  478. suspendThreshold: suspendThreshold.quantity,
  479. sensitivity: insulinSensitivitySchedule,
  480. model: walshInsulinModel,
  481. basalRates: basalRateSchedule,
  482. maxBasalRate: maxBasalRate,
  483. lastTempBasal: nil
  484. )
  485. XCTAssertNil(dose)
  486. // Cancel last temp
  487. let lastTempBasal = DoseEntry(
  488. type: .tempBasal,
  489. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  490. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  491. value: 1.225,
  492. unit: .unitsPerHour
  493. )
  494. dose = glucose.recommendedTempBasal(
  495. to: glucoseTargetRange,
  496. at: glucose.first!.startDate,
  497. suspendThreshold: suspendThreshold.quantity,
  498. sensitivity: insulinSensitivitySchedule,
  499. model: walshInsulinModel,
  500. basalRates: basalRateSchedule,
  501. maxBasalRate: maxBasalRate,
  502. lastTempBasal: lastTempBasal
  503. )
  504. XCTAssertEqual(0, dose!.unitsPerHour)
  505. XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration)
  506. }
  507. func testStartLowEndHighAutomaticBolus() {
  508. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  509. var dose = glucose.recommendedAutomaticDose(
  510. to: glucoseTargetRange,
  511. at: glucose.first!.startDate,
  512. suspendThreshold: suspendThreshold.quantity,
  513. sensitivity: insulinSensitivitySchedule,
  514. model: walshInsulinModel,
  515. basalRates: basalRateSchedule,
  516. maxAutomaticBolus: 5,
  517. partialApplicationFactor: 0.5,
  518. lastTempBasal: nil
  519. )
  520. XCTAssertNil(dose)
  521. let lastTempBasal = DoseEntry(
  522. type: .tempBasal,
  523. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  524. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  525. value: 1.225,
  526. unit: .unitsPerHour
  527. )
  528. dose = glucose.recommendedAutomaticDose(
  529. to: glucoseTargetRange,
  530. at: glucose.first!.startDate,
  531. suspendThreshold: suspendThreshold.quantity,
  532. sensitivity: insulinSensitivitySchedule,
  533. model: walshInsulinModel,
  534. basalRates: basalRateSchedule,
  535. maxAutomaticBolus: 5,
  536. partialApplicationFactor: 0.5,
  537. lastTempBasal: lastTempBasal
  538. )
  539. XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour)
  540. XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration)
  541. XCTAssertEqual(0, dose!.bolusUnits!)
  542. }
  543. func testFlatAndHigh() {
  544. let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
  545. let dose = glucose.recommendedTempBasal(
  546. to: glucoseTargetRange,
  547. at: glucose.first!.startDate,
  548. suspendThreshold: suspendThreshold.quantity,
  549. sensitivity: insulinSensitivitySchedule,
  550. model: walshInsulinModel,
  551. basalRates: basalRateSchedule,
  552. maxBasalRate: maxBasalRate,
  553. lastTempBasal: nil
  554. )
  555. XCTAssertEqual(3.0, dose!.unitsPerHour)
  556. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  557. }
  558. func testFlatAndHighAutomaticBolus() {
  559. let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
  560. let dose = glucose.recommendedAutomaticDose(
  561. to: glucoseTargetRange,
  562. at: glucose.first!.startDate,
  563. suspendThreshold: suspendThreshold.quantity,
  564. sensitivity: insulinSensitivitySchedule,
  565. model: walshInsulinModel,
  566. basalRates: basalRateSchedule,
  567. maxAutomaticBolus: 5,
  568. partialApplicationFactor: 0.5,
  569. lastTempBasal: nil
  570. )
  571. XCTAssertNil(dose!.basalAdjustment)
  572. XCTAssertEqual(0.85, dose!.bolusUnits!, accuracy: 1.0 / 40.0)
  573. }
  574. func testFlatAndHighAutomaticBolusWithOverride() {
  575. let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
  576. var dose = glucose.recommendedAutomaticDose(
  577. to: glucoseTargetRange,
  578. at: glucose.first!.startDate,
  579. suspendThreshold: suspendThreshold.quantity,
  580. sensitivity: insulinSensitivitySchedule,
  581. model: walshInsulinModel,
  582. basalRates: basalRateSchedule,
  583. maxAutomaticBolus: 5,
  584. partialApplicationFactor: 0.5,
  585. lastTempBasal: nil,
  586. isBasalRateScheduleOverrideActive: true
  587. )
  588. XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0)
  589. XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration)
  590. XCTAssertEqual(0.85, dose!.bolusUnits!, accuracy: 1.0 / 40.0)
  591. // Continue temp
  592. let lastTempBasal = DoseEntry(
  593. type: .tempBasal,
  594. startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)),
  595. endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)),
  596. value: 0.8,
  597. unit: .unitsPerHour
  598. )
  599. dose = glucose.recommendedAutomaticDose(
  600. to: glucoseTargetRange,
  601. at: glucose.first!.startDate,
  602. suspendThreshold: suspendThreshold.quantity,
  603. sensitivity: insulinSensitivitySchedule,
  604. model: walshInsulinModel,
  605. basalRates: basalRateSchedule,
  606. maxAutomaticBolus: 5,
  607. partialApplicationFactor: 0.5,
  608. lastTempBasal: lastTempBasal,
  609. isBasalRateScheduleOverrideActive: true
  610. )
  611. XCTAssertNil(dose!.basalAdjustment)
  612. XCTAssertEqual(0.85, dose!.bolusUnits!, accuracy: 1.0 / 40.0)
  613. }
  614. func testHighAndFalling() {
  615. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
  616. let insulinModel = WalshInsulinModel(actionDuration: insulinActionDuration, delay: 0)
  617. let dose = glucose.recommendedTempBasal(
  618. to: glucoseTargetRange,
  619. at: glucose.first!.startDate,
  620. suspendThreshold: suspendThreshold.quantity,
  621. sensitivity: insulinSensitivitySchedule,
  622. model: insulinModel,
  623. basalRates: basalRateSchedule,
  624. maxBasalRate: maxBasalRate,
  625. lastTempBasal: nil
  626. )
  627. XCTAssertEqual(1.425, dose!.unitsPerHour, accuracy: 1.0 / 40.0)
  628. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  629. }
  630. func testHighAndFallingAutomaticBolus() {
  631. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
  632. let dose = glucose.recommendedAutomaticDose(
  633. to: glucoseTargetRange,
  634. at: glucose.first!.startDate,
  635. suspendThreshold: suspendThreshold.quantity,
  636. sensitivity: insulinSensitivitySchedule,
  637. model: walshInsulinModel,
  638. basalRates: basalRateSchedule,
  639. maxAutomaticBolus: 5,
  640. partialApplicationFactor: 0.5,
  641. lastTempBasal: nil
  642. )
  643. XCTAssertNil(dose!.basalAdjustment)
  644. XCTAssertEqual(0.2, dose!.bolusUnits!, accuracy: 1.0 / 40.0)
  645. }
  646. func testHighAndFallingExponentialModel() {
  647. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
  648. let dose = glucose.recommendedTempBasal(
  649. to: glucoseTargetRange,
  650. at: glucose.first!.startDate,
  651. suspendThreshold: suspendThreshold.quantity,
  652. sensitivity: insulinSensitivitySchedule,
  653. model: exponentialInsulinModel,
  654. basalRates: basalRateSchedule,
  655. maxBasalRate: maxBasalRate,
  656. lastTempBasal: nil
  657. )
  658. XCTAssertEqual(2.68, dose!.unitsPerHour, accuracy: 1.0 / 40.0)
  659. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  660. }
  661. func testInRangeAndRisingAutomaticBolus() {
  662. let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising")
  663. let dose = glucose.recommendedAutomaticDose(
  664. to: glucoseTargetRange,
  665. at: glucose.first!.startDate,
  666. suspendThreshold: suspendThreshold.quantity,
  667. sensitivity: insulinSensitivitySchedule,
  668. model: walshInsulinModel,
  669. basalRates: basalRateSchedule,
  670. maxAutomaticBolus: 5,
  671. partialApplicationFactor: 0.5,
  672. lastTempBasal: nil
  673. )
  674. XCTAssertNil(dose!.basalAdjustment)
  675. XCTAssertEqual(0.2, dose!.bolusUnits!, accuracy: 1.0 / 40.0)
  676. }
  677. func testInRangeAndRising() {
  678. let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising")
  679. let dose = glucose.recommendedTempBasal(
  680. to: glucoseTargetRange,
  681. at: glucose.first!.startDate,
  682. suspendThreshold: suspendThreshold.quantity,
  683. sensitivity: insulinSensitivitySchedule,
  684. model: walshInsulinModel,
  685. basalRates: basalRateSchedule,
  686. maxBasalRate: maxBasalRate,
  687. lastTempBasal: nil
  688. )
  689. XCTAssertEqual(1.60, dose!.unitsPerHour, accuracy: 1.0 / 40.0)
  690. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  691. }
  692. func testHighAndRising() {
  693. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising")
  694. var dose = glucose.recommendedTempBasal(
  695. to: glucoseTargetRange,
  696. at: glucose.first!.startDate,
  697. suspendThreshold: suspendThreshold.quantity,
  698. sensitivity: self.insulinSensitivitySchedule,
  699. model: walshInsulinModel,
  700. basalRates: basalRateSchedule,
  701. maxBasalRate: maxBasalRate,
  702. lastTempBasal: nil
  703. )
  704. XCTAssertEqual(3.0, dose!.unitsPerHour)
  705. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  706. // Use mmol sensitivity value
  707. let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: HKUnit.millimolesPerLiter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 3.33)])!
  708. dose = glucose.recommendedTempBasal(
  709. to: glucoseTargetRange,
  710. at: glucose.first!.startDate,
  711. suspendThreshold: suspendThreshold.quantity,
  712. sensitivity: insulinSensitivitySchedule,
  713. model: walshInsulinModel,
  714. basalRates: basalRateSchedule,
  715. maxBasalRate: maxBasalRate,
  716. lastTempBasal: nil
  717. )
  718. XCTAssertEqual(2.975, dose!.unitsPerHour, accuracy: 1.0 / 40.0)
  719. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  720. }
  721. func testVeryLowAndRising() {
  722. let glucose = loadGlucoseValueFixture("recommend_temp_basal_very_low_end_in_range")
  723. let dose = glucose.recommendedTempBasal(
  724. to: glucoseTargetRange,
  725. at: glucose.first!.startDate,
  726. suspendThreshold: suspendThreshold.quantity,
  727. sensitivity: insulinSensitivitySchedule,
  728. model: walshInsulinModel,
  729. basalRates: basalRateSchedule,
  730. maxBasalRate: maxBasalRate,
  731. lastTempBasal: nil
  732. )
  733. XCTAssertEqual(0.0, dose!.unitsPerHour)
  734. XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
  735. }
  736. func testRiseAfterExponentialModelDIA() {
  737. let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast_after_6_hours")
  738. let dose = glucose.recommendedTempBasal(
  739. to: glucoseTargetRange,
  740. at: glucose.first!.startDate,
  741. suspendThreshold: suspendThreshold.quantity,
  742. sensitivity: insulinSensitivitySchedule,
  743. model: exponentialInsulinModel,
  744. basalRates: basalRateSchedule,
  745. maxBasalRate: maxBasalRate,
  746. lastTempBasal: nil
  747. )
  748. XCTAssertNil(dose)
  749. }
  750. func testRiseAfterDIA() {
  751. let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast")
  752. let dose = glucose.recommendedTempBasal(
  753. to: glucoseTargetRange,
  754. at: glucose.first!.startDate,
  755. suspendThreshold: suspendThreshold.quantity,
  756. sensitivity: insulinSensitivitySchedule,
  757. model: walshInsulinModel,
  758. basalRates: basalRateSchedule,
  759. maxBasalRate: maxBasalRate,
  760. lastTempBasal: nil
  761. )
  762. XCTAssertNil(dose)
  763. }
  764. func testNoInputGlucose() {
  765. let glucose: [SimpleGlucoseFixtureValue] = []
  766. let dose = glucose.recommendedTempBasal(
  767. to: glucoseTargetRange,
  768. suspendThreshold: suspendThreshold.quantity,
  769. sensitivity: insulinSensitivitySchedule,
  770. model: walshInsulinModel,
  771. basalRates: basalRateSchedule,
  772. maxBasalRate: maxBasalRate,
  773. lastTempBasal: nil
  774. )
  775. XCTAssertNil(dose)
  776. }
  777. }
  778. class RecommendBolusTests: XCTestCase {
  779. fileprivate let maxBolus = 10.0
  780. fileprivate let fortyIncrementsPerUnitRounder = { round($0 * 40) / 40 }
  781. func loadGlucoseValueFixture(_ resourceName: String) -> [SimpleGlucoseFixtureValue] {
  782. let fixture: [JSONDictionary] = loadFixture(resourceName)
  783. let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter()
  784. return fixture.map {
  785. return SimpleGlucoseFixtureValue(
  786. startDate: dateFormatter.date(from: $0["date"] as! String)!,
  787. quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double)
  788. )
  789. }
  790. }
  791. func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule {
  792. let fixture: [JSONDictionary] = loadFixture(resourceName)
  793. let items = fixture.map {
  794. return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double)
  795. }
  796. return BasalRateSchedule(dailyItems: items)!
  797. }
  798. var basalRateSchedule: BasalRateSchedule {
  799. return loadBasalRateScheduleFixture("read_selected_basal_profile")
  800. }
  801. var glucoseTargetRange: GlucoseRangeSchedule {
  802. return GlucoseRangeSchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 90, maxValue: 120))])!
  803. }
  804. var insulinSensitivitySchedule: InsulinSensitivitySchedule {
  805. return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])!
  806. }
  807. var suspendThreshold: GlucoseThreshold {
  808. return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 55)
  809. }
  810. var exponentialInsulinModel: InsulinModel = ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0, delay: 0)
  811. var walshInsulinModel: InsulinModel {
  812. return WalshInsulinModel(actionDuration: insulinActionDuration)
  813. }
  814. var insulinActionDuration: TimeInterval {
  815. return TimeInterval(hours: 4)
  816. }
  817. func testNoChange() {
  818. let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
  819. let dose = glucose.recommendedManualBolus(
  820. to: glucoseTargetRange,
  821. at: glucose.first!.startDate,
  822. suspendThreshold: suspendThreshold.quantity,
  823. sensitivity: insulinSensitivitySchedule,
  824. model: walshInsulinModel,
  825. pendingInsulin: 0,
  826. maxBolus: maxBolus
  827. )
  828. XCTAssertEqual(0, dose.amount)
  829. }
  830. func testStartHighEndInRange() {
  831. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range")
  832. let dose = glucose.recommendedManualBolus(
  833. to: glucoseTargetRange,
  834. at: glucose.first!.startDate,
  835. suspendThreshold: suspendThreshold.quantity,
  836. sensitivity: insulinSensitivitySchedule,
  837. model: walshInsulinModel,
  838. pendingInsulin: 0,
  839. maxBolus: maxBolus
  840. )
  841. XCTAssertEqual(0, dose.amount)
  842. }
  843. func testStartHighEndInRangeExponentialModel() {
  844. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range")
  845. let dose = glucose.recommendedManualBolus(
  846. to: glucoseTargetRange,
  847. at: glucose.first!.startDate,
  848. suspendThreshold: suspendThreshold.quantity,
  849. sensitivity: insulinSensitivitySchedule,
  850. model: exponentialInsulinModel,
  851. pendingInsulin: 0,
  852. maxBolus: maxBolus
  853. )
  854. XCTAssertEqual(0, dose.amount)
  855. }
  856. func testStartLowEndInRange() {
  857. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range")
  858. let dose = glucose.recommendedManualBolus(
  859. to: glucoseTargetRange,
  860. at: glucose.first!.startDate,
  861. suspendThreshold: suspendThreshold.quantity,
  862. sensitivity: insulinSensitivitySchedule,
  863. model: walshInsulinModel,
  864. pendingInsulin: 0,
  865. maxBolus: maxBolus
  866. )
  867. XCTAssertEqual(0, dose.amount)
  868. }
  869. func testStartHighEndLow() {
  870. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low")
  871. let dose = glucose.recommendedManualBolus(
  872. to: glucoseTargetRange,
  873. at: glucose.first!.startDate,
  874. suspendThreshold: suspendThreshold.quantity,
  875. sensitivity: insulinSensitivitySchedule,
  876. model: walshInsulinModel,
  877. pendingInsulin: 0,
  878. maxBolus: maxBolus
  879. )
  880. XCTAssertEqual(0, dose.amount)
  881. }
  882. func testStartLowEndHigh() {
  883. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  884. let dose = glucose.recommendedManualBolus(
  885. to: glucoseTargetRange,
  886. at: glucose.first!.startDate,
  887. suspendThreshold: suspendThreshold.quantity,
  888. sensitivity: insulinSensitivitySchedule,
  889. model: walshInsulinModel,
  890. pendingInsulin: 0,
  891. maxBolus: maxBolus,
  892. volumeRounder: fortyIncrementsPerUnitRounder
  893. )
  894. XCTAssertEqual(1.7, dose.amount)
  895. if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! {
  896. XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60)
  897. } else {
  898. XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
  899. }
  900. }
  901. func testStartLowEndHighExponentialModel() {
  902. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  903. let dose = glucose.recommendedManualBolus(
  904. to: glucoseTargetRange,
  905. at: glucose.first!.startDate,
  906. suspendThreshold: suspendThreshold.quantity,
  907. sensitivity: insulinSensitivitySchedule,
  908. model: exponentialInsulinModel,
  909. pendingInsulin: 0,
  910. maxBolus: maxBolus,
  911. volumeRounder: fortyIncrementsPerUnitRounder
  912. )
  913. XCTAssertEqual(1.9, dose.amount)
  914. if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! {
  915. XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60)
  916. } else {
  917. XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
  918. }
  919. }
  920. func testStartBelowSuspendThresholdEndHigh() {
  921. // 60 - 200 mg/dL
  922. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  923. let dose = glucose.recommendedManualBolus(
  924. to: glucoseTargetRange,
  925. at: glucose.first!.startDate,
  926. suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 70),
  927. sensitivity: insulinSensitivitySchedule,
  928. model: walshInsulinModel,
  929. pendingInsulin: 0,
  930. maxBolus: maxBolus
  931. )
  932. XCTAssertEqual(0, dose.amount)
  933. if case BolusRecommendationNotice.glucoseBelowSuspendThreshold(let glucose) = dose.notice! {
  934. XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60)
  935. } else {
  936. XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
  937. }
  938. }
  939. func testStartBelowSuspendThresholdEndHighExponentialModel() {
  940. // 60 - 200 mg/dL
  941. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  942. let dose = glucose.recommendedManualBolus(
  943. to: glucoseTargetRange,
  944. at: glucose.first!.startDate,
  945. suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 70),
  946. sensitivity: insulinSensitivitySchedule,
  947. model: exponentialInsulinModel,
  948. pendingInsulin: 0,
  949. maxBolus: maxBolus
  950. )
  951. XCTAssertEqual(0, dose.amount)
  952. if case BolusRecommendationNotice.glucoseBelowSuspendThreshold(let glucose) = dose.notice! {
  953. XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60)
  954. } else {
  955. XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
  956. }
  957. }
  958. func testStartLowNoSuspendThresholdEndHigh() {
  959. // 60 - 200 mg/dL
  960. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  961. let dose = glucose.recommendedManualBolus(
  962. to: glucoseTargetRange,
  963. at: glucose.first!.startDate,
  964. suspendThreshold: nil, // Expected to default to 90
  965. sensitivity: insulinSensitivitySchedule,
  966. model: walshInsulinModel,
  967. pendingInsulin: 0,
  968. maxBolus: maxBolus
  969. )
  970. XCTAssertEqual(0, dose.amount)
  971. if case BolusRecommendationNotice.glucoseBelowSuspendThreshold(let glucose) = dose.notice! {
  972. XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60)
  973. } else {
  974. XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
  975. }
  976. }
  977. func testDroppingBelowRangeThenRising() {
  978. let glucose = loadGlucoseValueFixture("recommend_temp_basal_dropping_then_rising")
  979. let dose = glucose.recommendedManualBolus(
  980. to: glucoseTargetRange,
  981. at: glucose.first!.startDate,
  982. suspendThreshold: suspendThreshold.quantity,
  983. sensitivity: insulinSensitivitySchedule,
  984. model: walshInsulinModel,
  985. pendingInsulin: 0,
  986. maxBolus: maxBolus,
  987. volumeRounder: fortyIncrementsPerUnitRounder
  988. )
  989. XCTAssertEqual(1.575, dose.amount)
  990. XCTAssertEqual(BolusRecommendationNotice.predictedGlucoseBelowTarget(minGlucose: glucose[1]), dose.notice!)
  991. }
  992. func testStartLowEndHighWithPendingBolus() {
  993. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  994. let dose = glucose.recommendedManualBolus(
  995. to: glucoseTargetRange,
  996. at: glucose.first!.startDate,
  997. suspendThreshold: suspendThreshold.quantity,
  998. sensitivity: insulinSensitivitySchedule,
  999. model: walshInsulinModel,
  1000. pendingInsulin: 1,
  1001. maxBolus: maxBolus,
  1002. volumeRounder: fortyIncrementsPerUnitRounder
  1003. )
  1004. XCTAssertEqual(0.7, dose.amount)
  1005. }
  1006. func testStartLowEndHighWithPendingBolusExponentialModel() {
  1007. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
  1008. let dose = glucose.recommendedManualBolus(
  1009. to: glucoseTargetRange,
  1010. at: glucose.first!.startDate,
  1011. suspendThreshold: suspendThreshold.quantity,
  1012. sensitivity: insulinSensitivitySchedule,
  1013. model: exponentialInsulinModel,
  1014. pendingInsulin: 1,
  1015. maxBolus: maxBolus,
  1016. volumeRounder: fortyIncrementsPerUnitRounder
  1017. )
  1018. XCTAssertEqual(0.9, dose.amount)
  1019. }
  1020. func testStartVeryLowEndHigh() {
  1021. let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_very_low_end_high")
  1022. let dose = glucose.recommendedManualBolus(
  1023. to: glucoseTargetRange,
  1024. at: glucose.first!.startDate,
  1025. suspendThreshold: suspendThreshold.quantity,
  1026. sensitivity: insulinSensitivitySchedule,
  1027. model: walshInsulinModel,
  1028. pendingInsulin: 0,
  1029. maxBolus: maxBolus
  1030. )
  1031. XCTAssertEqual(0, dose.amount)
  1032. }
  1033. func testFlatAndHigh() {
  1034. let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
  1035. let dose = glucose.recommendedManualBolus(
  1036. to: glucoseTargetRange,
  1037. at: glucose.first!.startDate,
  1038. suspendThreshold: suspendThreshold.quantity,
  1039. sensitivity: insulinSensitivitySchedule,
  1040. model: walshInsulinModel,
  1041. pendingInsulin: 0,
  1042. maxBolus: maxBolus
  1043. )
  1044. XCTAssertEqual(1.7, dose.amount, accuracy: 1.0 / 40.0)
  1045. }
  1046. func testHighAndFalling() {
  1047. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
  1048. let dose = glucose.recommendedManualBolus(
  1049. to: glucoseTargetRange,
  1050. at: glucose.first!.startDate,
  1051. suspendThreshold: suspendThreshold.quantity,
  1052. sensitivity: insulinSensitivitySchedule,
  1053. model: walshInsulinModel,
  1054. pendingInsulin: 0,
  1055. maxBolus: maxBolus
  1056. )
  1057. XCTAssertEqual(0.4, dose.amount, accuracy: 1.0 / 40.0)
  1058. }
  1059. func testHighAndFallingExponentialModel() {
  1060. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
  1061. let dose = glucose.recommendedManualBolus(
  1062. to: glucoseTargetRange,
  1063. at: glucose.first!.startDate,
  1064. suspendThreshold: suspendThreshold.quantity,
  1065. sensitivity: insulinSensitivitySchedule,
  1066. model: exponentialInsulinModel,
  1067. pendingInsulin: 0,
  1068. maxBolus: maxBolus
  1069. )
  1070. XCTAssertEqual(0.94, dose.amount, accuracy: 1.0 / 40.0)
  1071. }
  1072. func testInRangeAndRising() {
  1073. let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising")
  1074. var dose = glucose.recommendedManualBolus(
  1075. to: glucoseTargetRange,
  1076. at: glucose.first!.startDate,
  1077. suspendThreshold: suspendThreshold.quantity,
  1078. sensitivity: insulinSensitivitySchedule,
  1079. model: walshInsulinModel,
  1080. pendingInsulin: 0,
  1081. maxBolus: maxBolus
  1082. )
  1083. XCTAssertEqual(0.4, dose.amount, accuracy: 1.0 / 40.0)
  1084. // Less existing temp
  1085. dose = glucose.recommendedManualBolus(
  1086. to: glucoseTargetRange,
  1087. at: glucose.first!.startDate,
  1088. suspendThreshold: suspendThreshold.quantity,
  1089. sensitivity: insulinSensitivitySchedule,
  1090. model: walshInsulinModel,
  1091. pendingInsulin: 0.8,
  1092. maxBolus: maxBolus
  1093. )
  1094. XCTAssertEqual(0, dose.amount, accuracy: .ulpOfOne)
  1095. }
  1096. func testStartLowEndJustAboveRange() {
  1097. let glucose = loadGlucoseValueFixture("recommended_temp_start_low_end_just_above_range")
  1098. let dose = glucose.recommendedManualBolus(
  1099. to: glucoseTargetRange,
  1100. at: glucose.first!.startDate,
  1101. suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0),
  1102. sensitivity: insulinSensitivitySchedule,
  1103. model: exponentialInsulinModel,
  1104. pendingInsulin: 0,
  1105. maxBolus: maxBolus,
  1106. volumeRounder: fortyIncrementsPerUnitRounder
  1107. )
  1108. XCTAssertEqual(0.275, dose.amount)
  1109. }
  1110. func testHighAndRising() {
  1111. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising")
  1112. var dose = glucose.recommendedManualBolus(
  1113. to: glucoseTargetRange,
  1114. at: glucose.first!.startDate,
  1115. suspendThreshold: suspendThreshold.quantity,
  1116. sensitivity: self.insulinSensitivitySchedule,
  1117. model: walshInsulinModel,
  1118. pendingInsulin: 0,
  1119. maxBolus: maxBolus
  1120. )
  1121. XCTAssertEqual(1.35, dose.amount, accuracy: 1.0 / 40.0)
  1122. // Use mmol sensitivity value
  1123. let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: HKUnit.millimolesPerLiter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 10.0 / 3)])!
  1124. dose = glucose.recommendedManualBolus(
  1125. to: glucoseTargetRange,
  1126. at: glucose.first!.startDate,
  1127. suspendThreshold: suspendThreshold.quantity,
  1128. sensitivity: insulinSensitivitySchedule,
  1129. model: walshInsulinModel,
  1130. pendingInsulin: 0,
  1131. maxBolus: maxBolus
  1132. )
  1133. XCTAssertEqual(1.35, dose.amount, accuracy: 1.0 / 40.0)
  1134. }
  1135. func testHighAndRisingExponentialModel() {
  1136. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising")
  1137. var dose = glucose.recommendedManualBolus(
  1138. to: glucoseTargetRange,
  1139. at: glucose.first!.startDate,
  1140. suspendThreshold: suspendThreshold.quantity,
  1141. sensitivity: self.insulinSensitivitySchedule,
  1142. model: exponentialInsulinModel,
  1143. pendingInsulin: 0,
  1144. maxBolus: maxBolus
  1145. )
  1146. XCTAssertEqual(1.95, dose.amount, accuracy: 1.0 / 40.0)
  1147. // Use mmol sensitivity value
  1148. let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: HKUnit.millimolesPerLiter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 10.0 / 3)])!
  1149. dose = glucose.recommendedManualBolus(
  1150. to: glucoseTargetRange,
  1151. at: glucose.first!.startDate,
  1152. suspendThreshold: suspendThreshold.quantity,
  1153. sensitivity: insulinSensitivitySchedule,
  1154. model: exponentialInsulinModel,
  1155. pendingInsulin: 0,
  1156. maxBolus: maxBolus
  1157. )
  1158. XCTAssertEqual(1.95, dose.amount, accuracy: 1.0 / 40.0)
  1159. }
  1160. func testRiseAfterDIA() {
  1161. let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast")
  1162. let dose = glucose.recommendedManualBolus(
  1163. to: glucoseTargetRange,
  1164. at: glucose.first!.startDate,
  1165. suspendThreshold: suspendThreshold.quantity,
  1166. sensitivity: insulinSensitivitySchedule,
  1167. model: walshInsulinModel,
  1168. pendingInsulin: 0,
  1169. maxBolus: maxBolus
  1170. )
  1171. XCTAssertEqual(0.0, dose.amount)
  1172. }
  1173. func testRiseAfterExponentialModelDIA() {
  1174. let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast_after_6_hours")
  1175. let dose = glucose.recommendedManualBolus(
  1176. to: glucoseTargetRange,
  1177. at: glucose.first!.startDate,
  1178. suspendThreshold: suspendThreshold.quantity,
  1179. sensitivity: insulinSensitivitySchedule,
  1180. model: walshInsulinModel,
  1181. pendingInsulin: 0,
  1182. maxBolus: maxBolus
  1183. )
  1184. XCTAssertEqual(0.0, dose.amount)
  1185. }
  1186. func testNoInputGlucose() {
  1187. let glucose: [SimpleGlucoseFixtureValue] = []
  1188. let dose = glucose.recommendedManualBolus(
  1189. to: glucoseTargetRange,
  1190. suspendThreshold: suspendThreshold.quantity,
  1191. sensitivity: insulinSensitivitySchedule,
  1192. model: walshInsulinModel,
  1193. pendingInsulin: 0,
  1194. maxBolus: maxBolus
  1195. )
  1196. XCTAssertEqual(0, dose.amount)
  1197. }
  1198. func testDoseWithFiaspCurve() {
  1199. let glucose = loadGlucoseValueFixture("recommended_temp_start_low_end_just_above_range")
  1200. let dose = glucose.recommendedManualBolus(
  1201. to: glucoseTargetRange,
  1202. at: glucose.first!.startDate,
  1203. suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0),
  1204. sensitivity: insulinSensitivitySchedule,
  1205. model: ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 3300.0),
  1206. pendingInsulin: 0,
  1207. maxBolus: maxBolus,
  1208. volumeRounder: fortyIncrementsPerUnitRounder
  1209. )
  1210. XCTAssertEqual(0.375, dose.amount)
  1211. }
  1212. func testDoseWithChildCurve() {
  1213. let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising")
  1214. let dose = glucose.recommendedManualBolus(
  1215. to: glucoseTargetRange,
  1216. at: glucose.first!.startDate,
  1217. suspendThreshold: suspendThreshold.quantity,
  1218. sensitivity: self.insulinSensitivitySchedule,
  1219. model: ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 3900.0),
  1220. pendingInsulin: 0,
  1221. maxBolus: maxBolus
  1222. )
  1223. XCTAssertEqual(1.96, dose.amount, accuracy: 1.0 / 40.0)
  1224. }
  1225. }