CarbMathTests.swift 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. //
  2. // CarbMathTests.swift
  3. // CarbKitTests
  4. //
  5. // Created by Nathan Racklyeft on 1/18/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import XCTest
  9. @testable import LoopKit
  10. import HealthKit
  11. class CarbMathTests: XCTestCase {
  12. private func printCarbValues(_ carbValues: [CarbValue]) {
  13. let unit = HKUnit.gram()
  14. print("\n\n")
  15. print(String(data: try! JSONSerialization.data(
  16. withJSONObject: carbValues.map({ (value) -> [String: Any] in
  17. return [
  18. "date": ISO8601DateFormatter.localTimeDate().string(from: value.startDate),
  19. "amount": value.quantity.doubleValue(for: unit),
  20. "unit": "g"
  21. ]
  22. }),
  23. options: .prettyPrinted), encoding: .utf8)!)
  24. print("\n\n")
  25. }
  26. private func loadSchedules() -> (CarbRatioSchedule, InsulinSensitivitySchedule) {
  27. let fixture: JSONDictionary = loadFixture("read_carb_ratios")
  28. let schedule = fixture["schedule"] as! [JSONDictionary]
  29. let items = schedule.map {
  30. return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["offset"] as! Double), value: $0["ratio"] as! Double)
  31. }
  32. return (
  33. CarbRatioSchedule(unit: HKUnit.gram(), dailyItems: items)!,
  34. InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 40.0)])!
  35. )
  36. }
  37. private func loadHistoryFixture(_ name: String) -> [NewCarbEntry] {
  38. let fixture: [JSONDictionary] = loadFixture(name)
  39. return carbEntriesFromFixture(fixture)
  40. }
  41. private func loadCarbEntryFixture() -> [NewCarbEntry] {
  42. let fixture: [JSONDictionary] = loadFixture("carb_entry_input")
  43. return carbEntriesFromFixture(fixture)
  44. }
  45. private func carbEntriesFromFixture(_ fixture: [JSONDictionary]) -> [NewCarbEntry] {
  46. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  47. return fixture.map {
  48. let absorptionTime: TimeInterval?
  49. if let absorptionTimeMinutes = $0["absorption_time"] as? Double {
  50. absorptionTime = TimeInterval(minutes: absorptionTimeMinutes)
  51. } else {
  52. absorptionTime = nil
  53. }
  54. return NewCarbEntry(
  55. quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue: $0["amount"] as! Double),
  56. startDate: dateFormatter.date(from: $0["start_at"] as! String)!,
  57. foodType: nil,
  58. absorptionTime: absorptionTime
  59. )
  60. }
  61. }
  62. private func loadEffectOutputFixture() -> [GlucoseEffect] {
  63. let fixture: [JSONDictionary] = loadFixture("carb_effect_from_history_output")
  64. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  65. return fixture.map {
  66. return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double))
  67. }
  68. }
  69. private func loadCOBOutputFixture(_ name: String) -> [CarbValue] {
  70. let fixture: [JSONDictionary] = loadFixture(name)
  71. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  72. return fixture.map {
  73. return CarbValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double))
  74. }
  75. }
  76. private func loadICEInputFixture(_ name: String) -> [GlucoseEffectVelocity] {
  77. let fixture: [JSONDictionary] = loadFixture(name)
  78. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  79. let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
  80. return fixture.map {
  81. let quantity = HKQuantity(unit: unit, doubleValue: $0["velocity"] as! Double)
  82. return GlucoseEffectVelocity(
  83. startDate: dateFormatter.date(from: $0["start_at"] as! String)!,
  84. endDate: dateFormatter.date(from: $0["end_at"] as! String)!,
  85. quantity: quantity)
  86. }
  87. }
  88. func testCarbEffectWithZeroEntry() {
  89. let inputICE = loadICEInputFixture("ice_35_min_input")
  90. let (carbRatios, insulinSensitivities) = loadSchedules()
  91. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  92. fast: TimeInterval(hours: 1),
  93. medium: TimeInterval(hours: 2),
  94. slow: TimeInterval(hours: 4)
  95. )
  96. let carbEntry = NewCarbEntry(
  97. quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: 0),
  98. startDate: inputICE[0].startDate,
  99. foodType: nil,
  100. absorptionTime: TimeInterval(minutes: 120)
  101. )
  102. let statuses = [carbEntry].map(
  103. to: inputICE,
  104. carbRatio: carbRatios,
  105. insulinSensitivity: insulinSensitivities,
  106. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  107. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  108. delay: TimeInterval(minutes: 0),
  109. initialAbsorptionTimeOverrun: 1.5,
  110. absorptionModel: LinearAbsorption(),
  111. adaptiveAbsorptionRateEnabled: false,
  112. adaptiveRateStandbyIntervalFraction: 0.2
  113. )
  114. XCTAssertEqual(statuses.count, 1)
  115. XCTAssertEqual(statuses[0].absorption?.estimatedTimeRemaining, 0)
  116. }
  117. func testCarbEffectFromHistory() {
  118. let input = loadHistoryFixture("carb_effect_from_history_input")
  119. let output = loadEffectOutputFixture()
  120. let (carbRatios, insulinSensitivities) = loadSchedules()
  121. let effects = input.glucoseEffects(carbRatios: carbRatios, insulinSensitivities: insulinSensitivities, defaultAbsorptionTime: TimeInterval(minutes: 180), absorptionModel: ParabolicAbsorption())
  122. XCTAssertEqual(output.count, effects.count)
  123. for (expected, calculated) in zip(output, effects) {
  124. XCTAssertEqual(expected.startDate, calculated.startDate)
  125. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne))
  126. }
  127. }
  128. func testCarbsOnBoardFromHistory() {
  129. let input = loadHistoryFixture("carb_effect_from_history_input")
  130. let output = loadCOBOutputFixture("carbs_on_board_output")
  131. //CarbAbsorptionModel.settings = CarbModelSettings(absorptionModel: ParabolicAbsorption(), initialAbsorptionTimeOverrun: 1.5, adaptiveAbsorptionRateEnabled: false)
  132. let cob = input.carbsOnBoard(defaultAbsorptionTime: TimeInterval(minutes: 180), absorptionModel: ParabolicAbsorption(), delay: TimeInterval(minutes: 10), delta: TimeInterval(minutes: 5))
  133. XCTAssertEqual(output.count, cob.count)
  134. for (expected, calculated) in zip(output, cob) {
  135. XCTAssertEqual(expected.startDate, calculated.startDate)
  136. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  137. }
  138. }
  139. func testDynamicAbsorptionNoneObserved() {
  140. let inputICE = loadICEInputFixture("ice_35_min_input")
  141. let carbEntries = loadCarbEntryFixture()
  142. let output = loadCOBOutputFixture("ice_35_min_none_output")
  143. let (carbRatios, insulinSensitivities) = loadSchedules()
  144. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  145. fast: TimeInterval(hours: 1),
  146. medium: TimeInterval(hours: 2),
  147. slow: TimeInterval(hours: 4)
  148. )
  149. let futureCarbEntry = carbEntries[2]
  150. let statuses = [futureCarbEntry].map(
  151. to: inputICE,
  152. carbRatio: carbRatios,
  153. insulinSensitivity: insulinSensitivities,
  154. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  155. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  156. delay: TimeInterval(minutes: 0),
  157. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  158. absorptionModel: LinearAbsorption(),
  159. adaptiveAbsorptionRateEnabled: false,
  160. adaptiveRateStandbyIntervalFraction: 0.2
  161. )
  162. XCTAssertEqual(statuses.count, 1)
  163. // Full absorption remains
  164. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, TimeInterval(hours: 4), accuracy: 1)
  165. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  166. from: inputICE[0].startDate,
  167. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  168. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  169. absorptionModel: LinearAbsorption(),
  170. delay: TimeInterval(minutes: 10),
  171. delta: TimeInterval(minutes: 5))
  172. let unit = HKUnit.gram()
  173. XCTAssertEqual(output.count, carbsOnBoard.count)
  174. for (expected, calculated) in zip(output, carbsOnBoard) {
  175. XCTAssertEqual(expected.startDate, calculated.startDate)
  176. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  177. }
  178. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  179. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  180. }
  181. func testDynamicAbsorptionPartiallyObserved() {
  182. let inputICE = loadICEInputFixture("ice_35_min_input")
  183. let carbEntries = loadCarbEntryFixture()
  184. let output = loadCOBOutputFixture("ice_35_min_partial_output")
  185. let (carbRatios, insulinSensitivities) = loadSchedules()
  186. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  187. fast: TimeInterval(hours: 1),
  188. medium: TimeInterval(hours: 2),
  189. slow: TimeInterval(hours: 4)
  190. )
  191. let statuses = [carbEntries[0]].map(
  192. to: inputICE,
  193. carbRatio: carbRatios,
  194. insulinSensitivity: insulinSensitivities,
  195. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  196. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  197. delay: TimeInterval(minutes: 0),
  198. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  199. absorptionModel: LinearAbsorption(),
  200. adaptiveAbsorptionRateEnabled: false,
  201. adaptiveRateStandbyIntervalFraction: 0.2)
  202. XCTAssertEqual(statuses.count, 1)
  203. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 8509, accuracy: 1)
  204. let absorption = statuses[0].absorption!
  205. let unit = HKUnit.gram()
  206. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  207. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  208. from: inputICE[0].startDate,
  209. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  210. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  211. absorptionModel: LinearAbsorption(),
  212. delay: TimeInterval(minutes: 10),
  213. delta: TimeInterval(minutes: 5)
  214. )
  215. XCTAssertEqual(output.count, carbsOnBoard.count)
  216. for (expected, calculated) in zip(output, carbsOnBoard) {
  217. XCTAssertEqual(expected.startDate, calculated.startDate)
  218. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  219. }
  220. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  221. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  222. XCTAssertEqual(carbsOnBoard[25].quantity.doubleValue(for: unit), 9, accuracy: 1)
  223. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  224. }
  225. func testDynamicAbsorptionFullyObserved() {
  226. let inputICE = loadICEInputFixture("ice_1_hour_input")
  227. let carbEntries = loadCarbEntryFixture()
  228. let output = loadCOBOutputFixture("ice_1_hour_output")
  229. let (carbRatios, insulinSensitivities) = loadSchedules()
  230. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  231. fast: TimeInterval(hours: 1),
  232. medium: TimeInterval(hours: 2),
  233. slow: TimeInterval(hours: 4)
  234. )
  235. let statuses = [carbEntries[0]].map(
  236. to: inputICE,
  237. carbRatio: carbRatios,
  238. insulinSensitivity: insulinSensitivities,
  239. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  240. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  241. delay: TimeInterval(minutes: 0),
  242. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  243. absorptionModel: LinearAbsorption(),
  244. adaptiveAbsorptionRateEnabled: false,
  245. adaptiveRateStandbyIntervalFraction: 0.2
  246. )
  247. XCTAssertEqual(statuses.count, 1)
  248. XCTAssertNotNil(statuses[0].absorption)
  249. // No remaining absorption
  250. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 0, accuracy: 1)
  251. let absorption = statuses[0].absorption!
  252. let unit = HKUnit.gram()
  253. // All should be absorbed
  254. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 44, accuracy: 1)
  255. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  256. from: inputICE[0].startDate,
  257. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  258. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  259. absorptionModel: LinearAbsorption(),
  260. delay: TimeInterval(minutes: 10),
  261. delta: TimeInterval(minutes: 5)
  262. )
  263. XCTAssertEqual(output.count, carbsOnBoard.count)
  264. for (expected, calculated) in zip(output, carbsOnBoard) {
  265. XCTAssertEqual(expected.startDate, calculated.startDate)
  266. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  267. }
  268. XCTAssertEqual(carbsOnBoard[0].quantity.doubleValue(for: unit), 0, accuracy: 1)
  269. XCTAssertEqual(carbsOnBoard[1].quantity.doubleValue(for: unit), 44, accuracy: 1)
  270. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  271. XCTAssertEqual(carbsOnBoard[10].quantity.doubleValue(for: unit), 21, accuracy: 1)
  272. XCTAssertEqual(carbsOnBoard[17].quantity.doubleValue(for: unit), 7, accuracy: 1)
  273. XCTAssertEqual(carbsOnBoard[18].quantity.doubleValue(for: unit), 4, accuracy: 1)
  274. XCTAssertEqual(carbsOnBoard[30].quantity.doubleValue(for: unit), 0, accuracy: 1)
  275. }
  276. func testDynamicAbsorptionNeverFullyObserved() {
  277. let inputICE = loadICEInputFixture("ice_slow_absorption")
  278. let carbEntries = loadCarbEntryFixture()
  279. let output = loadCOBOutputFixture("ice_slow_absorption_output")
  280. let (carbRatios, insulinSensitivities) = loadSchedules()
  281. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  282. fast: TimeInterval(hours: 1),
  283. medium: TimeInterval(hours: 2),
  284. slow: TimeInterval(hours: 4)
  285. )
  286. let statuses = [carbEntries[1]].map(
  287. to: inputICE,
  288. carbRatio: carbRatios,
  289. insulinSensitivity: insulinSensitivities,
  290. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  291. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  292. delay: TimeInterval(minutes: 0),
  293. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  294. absorptionModel: LinearAbsorption(),
  295. adaptiveAbsorptionRateEnabled: false,
  296. adaptiveRateStandbyIntervalFraction: 0.2)
  297. XCTAssertEqual(statuses.count, 1)
  298. XCTAssertNotNil(statuses[0].absorption)
  299. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 10488, accuracy: 1)
  300. // Check 12 hours later
  301. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  302. from: inputICE[0].startDate,
  303. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 18)),
  304. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  305. absorptionModel: LinearAbsorption(),
  306. delay: TimeInterval(minutes: 10),
  307. delta: TimeInterval(minutes: 5)
  308. )
  309. let unit = HKUnit.gram()
  310. XCTAssertEqual(output.count, carbsOnBoard.count)
  311. for (expected, calculated) in zip(output, carbsOnBoard) {
  312. XCTAssertEqual(expected.startDate, calculated.startDate)
  313. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  314. }
  315. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  316. XCTAssertEqual(carbsOnBoard[5].quantity.doubleValue(for: unit), 30, accuracy: 1)
  317. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  318. }
  319. func testGroupedByOverlappingAbsorptionTimeFromHistory() {
  320. let input = loadHistoryFixture("grouped_by_overlapping_absorption_times_input")
  321. let outputFixture: [[JSONDictionary]] = loadFixture("grouped_by_overlapping_absorption_times_output")
  322. let output = outputFixture.map { self.carbEntriesFromFixture($0) }
  323. let grouped = input.groupedByOverlappingAbsorptionTimes(defaultAbsorptionTime: TimeInterval(minutes: 180))
  324. XCTAssertEqual(output.count, grouped.count)
  325. for (expected, calculated) in zip(output, grouped) {
  326. XCTAssertEqual(expected, calculated)
  327. }
  328. }
  329. func testGroupedByOverlappingAbsorptionTimeEdgeCases() {
  330. let input = loadHistoryFixture("grouped_by_overlapping_absorption_times_border_case_input")
  331. let outputFixture: [[JSONDictionary]] = loadFixture("grouped_by_overlapping_absorption_times_border_case_output")
  332. let output = outputFixture.map { self.carbEntriesFromFixture($0) }
  333. let grouped = input.groupedByOverlappingAbsorptionTimes(defaultAbsorptionTime: TimeInterval(minutes: 180))
  334. XCTAssertEqual(output.count, grouped.count)
  335. for (expected, calculated) in zip(output, grouped) {
  336. XCTAssertEqual(expected, calculated)
  337. }
  338. }
  339. // Aditional tests for nonlinear and adaptive-rate carb absorption models
  340. func testDynamicAbsorptionPiecewiseLinearNoneObserved() {
  341. let inputICE = loadICEInputFixture("ice_35_min_input")
  342. let carbEntries = loadCarbEntryFixture()
  343. let output = loadCOBOutputFixture("ice_35_min_none_piecewiselinear_output")
  344. let (carbRatios, insulinSensitivities) = loadSchedules()
  345. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  346. fast: TimeInterval(hours: 1),
  347. medium: TimeInterval(hours: 2),
  348. slow: TimeInterval(hours: 4)
  349. )
  350. let futureCarbEntry = carbEntries[2]
  351. let statuses = [futureCarbEntry].map(
  352. to: inputICE,
  353. carbRatio: carbRatios,
  354. insulinSensitivity: insulinSensitivities,
  355. absorptionTimeOverrun: 1.5,
  356. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  357. delay: TimeInterval(minutes: 0),
  358. initialAbsorptionTimeOverrun: 1.5,
  359. absorptionModel: PiecewiseLinearAbsorption(),
  360. adaptiveAbsorptionRateEnabled: false,
  361. adaptiveRateStandbyIntervalFraction: 0.2
  362. )
  363. XCTAssertEqual(statuses.count, 1)
  364. // Full absorption remains
  365. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, TimeInterval(hours: 3), accuracy: 1)
  366. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  367. from: inputICE[0].startDate,
  368. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  369. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  370. absorptionModel: PiecewiseLinearAbsorption(),
  371. delay: TimeInterval(minutes: 10),
  372. delta: TimeInterval(minutes: 5))
  373. let unit = HKUnit.gram()
  374. XCTAssertEqual(output.count, carbsOnBoard.count)
  375. for (expected, calculated) in zip(output, carbsOnBoard) {
  376. XCTAssertEqual(expected.startDate, calculated.startDate)
  377. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  378. }
  379. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  380. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  381. }
  382. func testDynamicAbsorptionPiecewiseLinearPartiallyObserved() {
  383. let inputICE = loadICEInputFixture("ice_35_min_input")
  384. let carbEntries = loadCarbEntryFixture()
  385. let output = loadCOBOutputFixture("ice_35_min_partial_piecewiselinear_output")
  386. let (carbRatios, insulinSensitivities) = loadSchedules()
  387. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  388. fast: TimeInterval(hours: 1),
  389. medium: TimeInterval(hours: 2),
  390. slow: TimeInterval(hours: 4)
  391. )
  392. let statuses = [carbEntries[0]].map(
  393. to: inputICE,
  394. carbRatio: carbRatios,
  395. insulinSensitivity: insulinSensitivities,
  396. absorptionTimeOverrun: 1.5,
  397. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  398. delay: TimeInterval(minutes: 0),
  399. initialAbsorptionTimeOverrun: 1.5,
  400. absorptionModel: PiecewiseLinearAbsorption(),
  401. adaptiveAbsorptionRateEnabled: false,
  402. adaptiveRateStandbyIntervalFraction: 0.2)
  403. XCTAssertEqual(statuses.count, 1)
  404. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 7008, accuracy: 1)
  405. let absorption = statuses[0].absorption!
  406. let unit = HKUnit.gram()
  407. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  408. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  409. from: inputICE[0].startDate,
  410. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  411. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  412. absorptionModel: PiecewiseLinearAbsorption(),
  413. delay: TimeInterval(minutes: 10),
  414. delta: TimeInterval(minutes: 5)
  415. )
  416. XCTAssertEqual(output.count, carbsOnBoard.count)
  417. for (expected, calculated) in zip(output, carbsOnBoard) {
  418. XCTAssertEqual(expected.startDate, calculated.startDate)
  419. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  420. }
  421. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  422. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  423. XCTAssertEqual(carbsOnBoard[20].quantity.doubleValue(for: unit), 5, accuracy: 1)
  424. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  425. }
  426. func testDynamicAbsorptionPiecewiseLinearFullyObserved() {
  427. let inputICE = loadICEInputFixture("ice_1_hour_input")
  428. let carbEntries = loadCarbEntryFixture()
  429. let output = loadCOBOutputFixture("ice_1_hour_output")
  430. let (carbRatios, insulinSensitivities) = loadSchedules()
  431. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  432. fast: TimeInterval(hours: 1),
  433. medium: TimeInterval(hours: 2),
  434. slow: TimeInterval(hours: 4)
  435. )
  436. //CarbAbsorptionModel.settings = CarbModelSettings(absorptionModel: PiecewiseLinearAbsorption(), initialAbsorptionTimeOverrun: 1.5, adaptiveAbsorptionRateEnabled: false)
  437. let statuses = [carbEntries[0]].map(
  438. to: inputICE,
  439. carbRatio: carbRatios,
  440. insulinSensitivity: insulinSensitivities,
  441. absorptionTimeOverrun: 1.5,
  442. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  443. delay: TimeInterval(minutes: 0),
  444. initialAbsorptionTimeOverrun: 1.5,
  445. absorptionModel: PiecewiseLinearAbsorption(),
  446. adaptiveAbsorptionRateEnabled: false,
  447. adaptiveRateStandbyIntervalFraction: 0.2
  448. )
  449. XCTAssertEqual(statuses.count, 1)
  450. XCTAssertNotNil(statuses[0].absorption)
  451. // No remaining absorption
  452. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 0, accuracy: 1)
  453. let absorption = statuses[0].absorption!
  454. let unit = HKUnit.gram()
  455. // All should be absorbed
  456. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 44, accuracy: 1)
  457. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  458. from: inputICE[0].startDate,
  459. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  460. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  461. absorptionModel: PiecewiseLinearAbsorption(),
  462. delay: TimeInterval(minutes: 10),
  463. delta: TimeInterval(minutes: 5)
  464. )
  465. XCTAssertEqual(output.count, carbsOnBoard.count)
  466. for (expected, calculated) in zip(output, carbsOnBoard) {
  467. XCTAssertEqual(expected.startDate, calculated.startDate)
  468. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  469. }
  470. XCTAssertEqual(carbsOnBoard[0].quantity.doubleValue(for: unit), 0, accuracy: 1)
  471. XCTAssertEqual(carbsOnBoard[1].quantity.doubleValue(for: unit), 44, accuracy: 1)
  472. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  473. XCTAssertEqual(carbsOnBoard[10].quantity.doubleValue(for: unit), 21, accuracy: 1)
  474. XCTAssertEqual(carbsOnBoard[17].quantity.doubleValue(for: unit), 7, accuracy: 1)
  475. XCTAssertEqual(carbsOnBoard[18].quantity.doubleValue(for: unit), 4, accuracy: 1)
  476. XCTAssertEqual(carbsOnBoard[30].quantity.doubleValue(for: unit), 0, accuracy: 1)
  477. }
  478. func testDynamicAbsorptionPiecewiseLinearNeverFullyObserved() {
  479. let inputICE = loadICEInputFixture("ice_slow_absorption")
  480. let carbEntries = loadCarbEntryFixture()
  481. let output = loadCOBOutputFixture("ice_slow_absorption_piecewiselinear_output")
  482. let (carbRatios, insulinSensitivities) = loadSchedules()
  483. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  484. fast: TimeInterval(hours: 1),
  485. medium: TimeInterval(hours: 2),
  486. slow: TimeInterval(hours: 4)
  487. )
  488. let statuses = [carbEntries[1]].map(
  489. to: inputICE,
  490. carbRatio: carbRatios,
  491. insulinSensitivity: insulinSensitivities,
  492. absorptionTimeOverrun: 1.5,
  493. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  494. delay: TimeInterval(minutes: 0),
  495. initialAbsorptionTimeOverrun: 1.5,
  496. absorptionModel: PiecewiseLinearAbsorption(),
  497. adaptiveAbsorptionRateEnabled: false,
  498. adaptiveRateStandbyIntervalFraction: 0.2)
  499. XCTAssertEqual(statuses.count, 1)
  500. XCTAssertNotNil(statuses[0].absorption)
  501. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 6888, accuracy: 1)
  502. // Check 12 hours later
  503. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  504. from: inputICE[0].startDate,
  505. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 18)),
  506. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  507. absorptionModel: PiecewiseLinearAbsorption(),
  508. delay: TimeInterval(minutes: 10),
  509. delta: TimeInterval(minutes: 5)
  510. )
  511. let unit = HKUnit.gram()
  512. XCTAssertEqual(output.count, carbsOnBoard.count)
  513. for (expected, calculated) in zip(output, carbsOnBoard) {
  514. XCTAssertEqual(expected.startDate, calculated.startDate)
  515. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  516. }
  517. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  518. XCTAssertEqual(carbsOnBoard[5].quantity.doubleValue(for: unit), 30, accuracy: 1)
  519. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  520. }
  521. func testDynamicAbsorptionPiecewiseLinearAdaptiveRatePartiallyObserved() {
  522. let inputICE = loadICEInputFixture("ice_35_min_input")
  523. let carbEntries = loadCarbEntryFixture()
  524. let output = loadCOBOutputFixture("ice_35_min_partial_piecewiselinear_adaptiverate_output")
  525. let (carbRatios, insulinSensitivities) = loadSchedules()
  526. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  527. fast: TimeInterval(hours: 1),
  528. medium: TimeInterval(hours: 2),
  529. slow: TimeInterval(hours: 4)
  530. )
  531. let statuses = [carbEntries[0]].map(
  532. to: inputICE,
  533. carbRatio: carbRatios,
  534. insulinSensitivity: insulinSensitivities,
  535. absorptionTimeOverrun: 1.5,
  536. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  537. delay: TimeInterval(minutes: 0),
  538. initialAbsorptionTimeOverrun: 1.0,
  539. absorptionModel: PiecewiseLinearAbsorption(),
  540. adaptiveAbsorptionRateEnabled: true,
  541. adaptiveRateStandbyIntervalFraction: 0.2)
  542. XCTAssertEqual(statuses.count, 1)
  543. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 3326, accuracy: 1)
  544. let absorption = statuses[0].absorption!
  545. let unit = HKUnit.gram()
  546. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  547. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  548. from: inputICE[0].startDate,
  549. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  550. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  551. absorptionModel: PiecewiseLinearAbsorption(),
  552. delay: TimeInterval(minutes: 10),
  553. delta: TimeInterval(minutes: 5)
  554. )
  555. XCTAssertEqual(output.count, carbsOnBoard.count)
  556. for (expected, calculated) in zip(output, carbsOnBoard) {
  557. XCTAssertEqual(expected.startDate, calculated.startDate)
  558. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  559. }
  560. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  561. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  562. XCTAssertEqual(carbsOnBoard[10].quantity.doubleValue(for: unit), 15, accuracy: 1)
  563. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  564. }
  565. }