CarbMathTests.swift 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941
  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. let startAt = dateFormatter.date(from: $0["start_at"] as! String)!
  55. return NewCarbEntry(
  56. date: startAt,
  57. quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue: $0["amount"] as! Double),
  58. startDate: startAt,
  59. foodType: nil,
  60. absorptionTime: absorptionTime
  61. )
  62. }
  63. }
  64. private func loadEffectOutputFixture(_ name: String) -> [GlucoseEffect] {
  65. let fixture: [JSONDictionary] = loadFixture(name)
  66. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  67. return fixture.map {
  68. return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double))
  69. }
  70. }
  71. private func loadCOBOutputFixture(_ name: String) -> [CarbValue] {
  72. let fixture: [JSONDictionary] = loadFixture(name)
  73. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  74. return fixture.map {
  75. return CarbValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double))
  76. }
  77. }
  78. private func loadICEInputFixture(_ name: String) -> [GlucoseEffectVelocity] {
  79. let fixture: [JSONDictionary] = loadFixture(name)
  80. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  81. let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
  82. return fixture.map {
  83. let quantity = HKQuantity(unit: unit, doubleValue: $0["velocity"] as! Double)
  84. return GlucoseEffectVelocity(
  85. startDate: dateFormatter.date(from: $0["start_at"] as! String)!,
  86. endDate: dateFormatter.date(from: $0["end_at"] as! String)!,
  87. quantity: quantity)
  88. }
  89. }
  90. func testCarbEffectWithZeroEntry() {
  91. let inputICE = loadICEInputFixture("ice_35_min_input")
  92. let (carbRatios, insulinSensitivities) = loadSchedules()
  93. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  94. fast: TimeInterval(hours: 1),
  95. medium: TimeInterval(hours: 2),
  96. slow: TimeInterval(hours: 4)
  97. )
  98. let startDate = inputICE[0].startDate
  99. let carbEntry = NewCarbEntry(
  100. date: startDate,
  101. quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: 0),
  102. startDate: startDate,
  103. foodType: nil,
  104. absorptionTime: TimeInterval(minutes: 120)
  105. )
  106. let statuses = [carbEntry].map(
  107. to: inputICE,
  108. carbRatio: carbRatios,
  109. insulinSensitivity: insulinSensitivities,
  110. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  111. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  112. delay: TimeInterval(minutes: 0),
  113. initialAbsorptionTimeOverrun: 1.5,
  114. absorptionModel: LinearAbsorption(),
  115. adaptiveAbsorptionRateEnabled: false,
  116. adaptiveRateStandbyIntervalFraction: 0.2
  117. )
  118. XCTAssertEqual(statuses.count, 1)
  119. XCTAssertEqual(statuses[0].absorption?.estimatedTimeRemaining, 0)
  120. }
  121. func testCarbEffectFromHistory() {
  122. let input = loadHistoryFixture("carb_effect_from_history_input")
  123. let output = loadEffectOutputFixture("carb_effect_from_history_output")
  124. let (carbRatios, insulinSensitivities) = loadSchedules()
  125. let effects = input.glucoseEffects(carbRatios: carbRatios, insulinSensitivities: insulinSensitivities, defaultAbsorptionTime: TimeInterval(minutes: 180), absorptionModel: ParabolicAbsorption())
  126. XCTAssertEqual(output.count, effects.count)
  127. for (expected, calculated) in zip(output, effects) {
  128. XCTAssertEqual(expected.startDate, calculated.startDate)
  129. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne))
  130. }
  131. }
  132. func testCarbsOnBoardFromHistory() {
  133. let input = loadHistoryFixture("carb_effect_from_history_input")
  134. let output = loadCOBOutputFixture("carbs_on_board_output")
  135. let cob = input.carbsOnBoard(defaultAbsorptionTime: TimeInterval(minutes: 180), absorptionModel: ParabolicAbsorption(), delay: TimeInterval(minutes: 10), delta: TimeInterval(minutes: 5))
  136. XCTAssertEqual(output.count, cob.count)
  137. for (expected, calculated) in zip(output, cob) {
  138. XCTAssertEqual(expected.startDate, calculated.startDate)
  139. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  140. }
  141. }
  142. func testDynamicGlucoseEffectAbsorptionNoneObserved() {
  143. let inputICE = loadICEInputFixture("ice_35_min_input")
  144. let carbEntries = loadCarbEntryFixture()
  145. let output = loadEffectOutputFixture("dynamic_glucose_effect_none_observed_output")
  146. let (carbRatios, insulinSensitivities) = loadSchedules()
  147. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  148. fast: TimeInterval(hours: 1),
  149. medium: TimeInterval(hours: 2),
  150. slow: TimeInterval(hours: 4)
  151. )
  152. let futureCarbEntry = carbEntries[2]
  153. let statuses = [futureCarbEntry].map(
  154. to: inputICE,
  155. carbRatio: carbRatios,
  156. insulinSensitivity: insulinSensitivities,
  157. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  158. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  159. delay: TimeInterval(minutes: 0),
  160. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  161. absorptionModel: LinearAbsorption(),
  162. adaptiveAbsorptionRateEnabled: false,
  163. adaptiveRateStandbyIntervalFraction: 0.2
  164. )
  165. XCTAssertEqual(statuses.count, 1)
  166. // Full absorption remains
  167. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, TimeInterval(hours: 4), accuracy: 1)
  168. let effects = statuses.dynamicGlucoseEffects(
  169. from: inputICE[0].startDate,
  170. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  171. carbRatios: carbRatios,
  172. insulinSensitivities: insulinSensitivities,
  173. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  174. absorptionModel: LinearAbsorption(),
  175. delay: 0
  176. )
  177. XCTAssertEqual(output.count, effects.count)
  178. for (expected, calculated) in zip(output, effects) {
  179. XCTAssertEqual(expected.startDate, calculated.startDate)
  180. XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne))
  181. }
  182. }
  183. func testDynamicAbsorptionNoneObserved() {
  184. let inputICE = loadICEInputFixture("ice_35_min_input")
  185. let carbEntries = loadCarbEntryFixture()
  186. let output = loadCOBOutputFixture("ice_35_min_none_output")
  187. let (carbRatios, insulinSensitivities) = loadSchedules()
  188. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  189. fast: TimeInterval(hours: 1),
  190. medium: TimeInterval(hours: 2),
  191. slow: TimeInterval(hours: 4)
  192. )
  193. let futureCarbEntry = carbEntries[2]
  194. let statuses = [futureCarbEntry].map(
  195. to: inputICE,
  196. carbRatio: carbRatios,
  197. insulinSensitivity: insulinSensitivities,
  198. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  199. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  200. delay: TimeInterval(minutes: 0),
  201. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  202. absorptionModel: LinearAbsorption(),
  203. adaptiveAbsorptionRateEnabled: false,
  204. adaptiveRateStandbyIntervalFraction: 0.2
  205. )
  206. XCTAssertEqual(statuses.count, 1)
  207. // Full absorption remains
  208. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, TimeInterval(hours: 4), accuracy: 1)
  209. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  210. from: inputICE[0].startDate,
  211. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  212. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  213. absorptionModel: LinearAbsorption(),
  214. delay: TimeInterval(minutes: 10),
  215. delta: TimeInterval(minutes: 5))
  216. let unit = HKUnit.gram()
  217. XCTAssertEqual(output.count, carbsOnBoard.count)
  218. for (expected, calculated) in zip(output, carbsOnBoard) {
  219. XCTAssertEqual(expected.startDate, calculated.startDate)
  220. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  221. }
  222. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  223. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  224. }
  225. func testDynamicAbsorptionPartiallyObserved() {
  226. let inputICE = loadICEInputFixture("ice_35_min_input")
  227. let carbEntries = loadCarbEntryFixture()
  228. let output = loadCOBOutputFixture("ice_35_min_partial_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. XCTAssertEqual(statuses.count, 1)
  247. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 8509, accuracy: 1)
  248. let absorption = statuses[0].absorption!
  249. let unit = HKUnit.gram()
  250. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  251. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  252. from: inputICE[0].startDate,
  253. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  254. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  255. absorptionModel: LinearAbsorption(),
  256. delay: TimeInterval(minutes: 10),
  257. delta: TimeInterval(minutes: 5)
  258. )
  259. XCTAssertEqual(output.count, carbsOnBoard.count)
  260. for (expected, calculated) in zip(output, carbsOnBoard) {
  261. XCTAssertEqual(expected.startDate, calculated.startDate)
  262. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  263. }
  264. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  265. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  266. XCTAssertEqual(carbsOnBoard[25].quantity.doubleValue(for: unit), 9, accuracy: 1)
  267. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  268. }
  269. func testDynamicGlucoseEffectAbsorptionPartiallyObserved() {
  270. let inputICE = loadICEInputFixture("ice_35_min_input")
  271. let carbEntries = loadCarbEntryFixture()
  272. let output = loadEffectOutputFixture("dynamic_glucose_effect_partially_observed_output")
  273. let (carbRatios, insulinSensitivities) = loadSchedules()
  274. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  275. fast: TimeInterval(hours: 1),
  276. medium: TimeInterval(hours: 2),
  277. slow: TimeInterval(hours: 4)
  278. )
  279. let statuses = [carbEntries[0]].map(
  280. to: inputICE,
  281. carbRatio: carbRatios,
  282. insulinSensitivity: insulinSensitivities,
  283. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  284. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  285. delay: TimeInterval(minutes: 0),
  286. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  287. absorptionModel: LinearAbsorption(),
  288. adaptiveAbsorptionRateEnabled: false,
  289. adaptiveRateStandbyIntervalFraction: 0.2)
  290. XCTAssertEqual(statuses.count, 1)
  291. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 8509, accuracy: 1)
  292. let absorption = statuses[0].absorption!
  293. let unit = HKUnit.gram()
  294. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  295. let effects = statuses.dynamicGlucoseEffects(
  296. from: inputICE[0].startDate,
  297. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  298. carbRatios: carbRatios,
  299. insulinSensitivities: insulinSensitivities,
  300. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  301. absorptionModel: LinearAbsorption()
  302. )
  303. XCTAssertEqual(output.count, effects.count)
  304. for (expected, calculated) in zip(output, effects) {
  305. XCTAssertEqual(expected.startDate, calculated.startDate)
  306. XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne))
  307. }
  308. }
  309. func testDynamicAbsorptionFullyObserved() {
  310. let inputICE = loadICEInputFixture("ice_1_hour_input")
  311. let carbEntries = loadCarbEntryFixture()
  312. let output = loadCOBOutputFixture("ice_1_hour_output")
  313. let (carbRatios, insulinSensitivities) = loadSchedules()
  314. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  315. fast: TimeInterval(hours: 1),
  316. medium: TimeInterval(hours: 2),
  317. slow: TimeInterval(hours: 4)
  318. )
  319. let statuses = [carbEntries[0]].map(
  320. to: inputICE,
  321. carbRatio: carbRatios,
  322. insulinSensitivity: insulinSensitivities,
  323. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  324. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  325. delay: TimeInterval(minutes: 0),
  326. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  327. absorptionModel: LinearAbsorption(),
  328. adaptiveAbsorptionRateEnabled: false,
  329. adaptiveRateStandbyIntervalFraction: 0.2
  330. )
  331. XCTAssertEqual(statuses.count, 1)
  332. XCTAssertNotNil(statuses[0].absorption)
  333. // No remaining absorption
  334. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 0, accuracy: 1)
  335. let absorption = statuses[0].absorption!
  336. let unit = HKUnit.gram()
  337. // All should be absorbed
  338. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 44, accuracy: 1)
  339. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  340. from: inputICE[0].startDate,
  341. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  342. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  343. absorptionModel: LinearAbsorption(),
  344. delay: TimeInterval(minutes: 10),
  345. delta: TimeInterval(minutes: 5)
  346. )
  347. XCTAssertEqual(output.count, carbsOnBoard.count)
  348. for (expected, calculated) in zip(output, carbsOnBoard) {
  349. XCTAssertEqual(expected.startDate, calculated.startDate)
  350. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  351. }
  352. XCTAssertEqual(carbsOnBoard[0].quantity.doubleValue(for: unit), 0, accuracy: 1)
  353. XCTAssertEqual(carbsOnBoard[1].quantity.doubleValue(for: unit), 44, accuracy: 1)
  354. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  355. XCTAssertEqual(carbsOnBoard[10].quantity.doubleValue(for: unit), 21, accuracy: 1)
  356. XCTAssertEqual(carbsOnBoard[17].quantity.doubleValue(for: unit), 7, accuracy: 1)
  357. XCTAssertEqual(carbsOnBoard[18].quantity.doubleValue(for: unit), 4, accuracy: 1)
  358. XCTAssertEqual(carbsOnBoard[30].quantity.doubleValue(for: unit), 0, accuracy: 1)
  359. }
  360. func testDynamicGlucoseEffectsAbsorptionFullyObserved() {
  361. let inputICE = loadICEInputFixture("ice_1_hour_input")
  362. let carbEntries = loadCarbEntryFixture()
  363. let output = loadEffectOutputFixture("dynamic_glucose_effect_fully_observed_output")
  364. let (carbRatios, insulinSensitivities) = loadSchedules()
  365. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  366. fast: TimeInterval(hours: 1),
  367. medium: TimeInterval(hours: 2),
  368. slow: TimeInterval(hours: 4)
  369. )
  370. let statuses = [carbEntries[0]].map(
  371. to: inputICE,
  372. carbRatio: carbRatios,
  373. insulinSensitivity: insulinSensitivities,
  374. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  375. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  376. delay: TimeInterval(minutes: 0),
  377. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  378. absorptionModel: LinearAbsorption(),
  379. adaptiveAbsorptionRateEnabled: false,
  380. adaptiveRateStandbyIntervalFraction: 0.2
  381. )
  382. XCTAssertEqual(statuses.count, 1)
  383. XCTAssertNotNil(statuses[0].absorption)
  384. // No remaining absorption
  385. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 0, accuracy: 1)
  386. let absorption = statuses[0].absorption!
  387. let unit = HKUnit.gram()
  388. // All should be absorbed
  389. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 44, accuracy: 1)
  390. let effects = statuses.dynamicGlucoseEffects(
  391. from: inputICE[0].startDate,
  392. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  393. carbRatios: carbRatios,
  394. insulinSensitivities: insulinSensitivities,
  395. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  396. absorptionModel: LinearAbsorption()
  397. )
  398. XCTAssertEqual(output.count, effects.count)
  399. for (expected, calculated) in zip(output, effects) {
  400. XCTAssertEqual(expected.startDate, calculated.startDate)
  401. XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne))
  402. }
  403. }
  404. func testDynamicAbsorptionNeverFullyObserved() {
  405. let inputICE = loadICEInputFixture("ice_slow_absorption")
  406. let carbEntries = loadCarbEntryFixture()
  407. let output = loadCOBOutputFixture("ice_slow_absorption_output")
  408. let (carbRatios, insulinSensitivities) = loadSchedules()
  409. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  410. fast: TimeInterval(hours: 1),
  411. medium: TimeInterval(hours: 2),
  412. slow: TimeInterval(hours: 4)
  413. )
  414. let statuses = [carbEntries[1]].map(
  415. to: inputICE,
  416. carbRatio: carbRatios,
  417. insulinSensitivity: insulinSensitivities,
  418. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  419. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  420. delay: TimeInterval(minutes: 0),
  421. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  422. absorptionModel: LinearAbsorption(),
  423. adaptiveAbsorptionRateEnabled: false,
  424. adaptiveRateStandbyIntervalFraction: 0.2)
  425. XCTAssertEqual(statuses.count, 1)
  426. XCTAssertNotNil(statuses[0].absorption)
  427. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 10488, accuracy: 1)
  428. // Check 12 hours later
  429. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  430. from: inputICE[0].startDate,
  431. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 18)),
  432. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  433. absorptionModel: LinearAbsorption(),
  434. delay: TimeInterval(minutes: 10),
  435. delta: TimeInterval(minutes: 5)
  436. )
  437. let unit = HKUnit.gram()
  438. XCTAssertEqual(output.count, carbsOnBoard.count)
  439. for (expected, calculated) in zip(output, carbsOnBoard) {
  440. XCTAssertEqual(expected.startDate, calculated.startDate)
  441. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  442. }
  443. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  444. XCTAssertEqual(carbsOnBoard[5].quantity.doubleValue(for: unit), 30, accuracy: 1)
  445. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  446. }
  447. func testDynamicGlucoseEffectsAbsorptionNeverFullyObserved() {
  448. let inputICE = loadICEInputFixture("ice_slow_absorption")
  449. let carbEntries = loadCarbEntryFixture()
  450. let output = loadEffectOutputFixture("dynamic_glucose_effect_never_fully_observed_output")
  451. let (carbRatios, insulinSensitivities) = loadSchedules()
  452. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  453. fast: TimeInterval(hours: 1),
  454. medium: TimeInterval(hours: 2),
  455. slow: TimeInterval(hours: 4)
  456. )
  457. let statuses = [carbEntries[1]].map(
  458. to: inputICE,
  459. carbRatio: carbRatios,
  460. insulinSensitivity: insulinSensitivities,
  461. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  462. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  463. delay: TimeInterval(minutes: 0),
  464. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  465. absorptionModel: LinearAbsorption(),
  466. adaptiveAbsorptionRateEnabled: false,
  467. adaptiveRateStandbyIntervalFraction: 0.2)
  468. XCTAssertEqual(statuses.count, 1)
  469. XCTAssertNotNil(statuses[0].absorption)
  470. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 10488, accuracy: 1)
  471. // Check 12 hours later
  472. let effects = statuses.dynamicGlucoseEffects(
  473. from: inputICE[0].startDate,
  474. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 18)),
  475. carbRatios: carbRatios,
  476. insulinSensitivities: insulinSensitivities,
  477. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  478. absorptionModel: LinearAbsorption()
  479. )
  480. XCTAssertEqual(output.count, effects.count)
  481. for (expected, calculated) in zip(output, effects) {
  482. XCTAssertEqual(expected.startDate, calculated.startDate)
  483. XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne))
  484. }
  485. }
  486. func testGroupedByOverlappingAbsorptionTimeFromHistory() {
  487. let input = loadHistoryFixture("grouped_by_overlapping_absorption_times_input")
  488. let outputFixture: [[JSONDictionary]] = loadFixture("grouped_by_overlapping_absorption_times_output")
  489. let output = outputFixture.map { self.carbEntriesFromFixture($0) }
  490. let grouped = input.groupedByOverlappingAbsorptionTimes(defaultAbsorptionTime: TimeInterval(minutes: 180))
  491. XCTAssertEqual(output.count, grouped.count)
  492. for (expected, calculated) in zip(output, grouped) {
  493. XCTAssertEqual(expected, calculated)
  494. }
  495. }
  496. func testGroupedByOverlappingAbsorptionTimeEdgeCases() {
  497. let input = loadHistoryFixture("grouped_by_overlapping_absorption_times_border_case_input")
  498. let outputFixture: [[JSONDictionary]] = loadFixture("grouped_by_overlapping_absorption_times_border_case_output")
  499. let output = outputFixture.map { self.carbEntriesFromFixture($0) }
  500. let grouped = input.groupedByOverlappingAbsorptionTimes(defaultAbsorptionTime: TimeInterval(minutes: 180))
  501. XCTAssertEqual(output.count, grouped.count)
  502. for (expected, calculated) in zip(output, grouped) {
  503. XCTAssertEqual(expected, calculated)
  504. }
  505. }
  506. // Aditional tests for nonlinear and adaptive-rate carb absorption models
  507. func testDynamicAbsorptionPiecewiseLinearNoneObserved() {
  508. let inputICE = loadICEInputFixture("ice_35_min_input")
  509. let carbEntries = loadCarbEntryFixture()
  510. let output = loadCOBOutputFixture("ice_35_min_none_piecewiselinear_output")
  511. let (carbRatios, insulinSensitivities) = loadSchedules()
  512. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  513. fast: TimeInterval(hours: 1),
  514. medium: TimeInterval(hours: 2),
  515. slow: TimeInterval(hours: 4)
  516. )
  517. let futureCarbEntry = carbEntries[2]
  518. let statuses = [futureCarbEntry].map(
  519. to: inputICE,
  520. carbRatio: carbRatios,
  521. insulinSensitivity: insulinSensitivities,
  522. absorptionTimeOverrun: 1.5,
  523. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  524. delay: TimeInterval(minutes: 0),
  525. initialAbsorptionTimeOverrun: 1.5,
  526. absorptionModel: PiecewiseLinearAbsorption(),
  527. adaptiveAbsorptionRateEnabled: false,
  528. adaptiveRateStandbyIntervalFraction: 0.2
  529. )
  530. XCTAssertEqual(statuses.count, 1)
  531. // Full absorption remains
  532. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, TimeInterval(hours: 3), accuracy: 1)
  533. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  534. from: inputICE[0].startDate,
  535. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  536. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  537. absorptionModel: PiecewiseLinearAbsorption(),
  538. delay: TimeInterval(minutes: 10),
  539. delta: TimeInterval(minutes: 5))
  540. let unit = HKUnit.gram()
  541. XCTAssertEqual(output.count, carbsOnBoard.count)
  542. for (expected, calculated) in zip(output, carbsOnBoard) {
  543. XCTAssertEqual(expected.startDate, calculated.startDate)
  544. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  545. }
  546. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  547. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  548. }
  549. func testDynamicAbsorptionPiecewiseLinearPartiallyObserved() {
  550. let inputICE = loadICEInputFixture("ice_35_min_input")
  551. let carbEntries = loadCarbEntryFixture()
  552. let output = loadCOBOutputFixture("ice_35_min_partial_piecewiselinear_output")
  553. let (carbRatios, insulinSensitivities) = loadSchedules()
  554. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  555. fast: TimeInterval(hours: 1),
  556. medium: TimeInterval(hours: 2),
  557. slow: TimeInterval(hours: 4)
  558. )
  559. let statuses = [carbEntries[0]].map(
  560. to: inputICE,
  561. carbRatio: carbRatios,
  562. insulinSensitivity: insulinSensitivities,
  563. absorptionTimeOverrun: 1.5,
  564. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  565. delay: TimeInterval(minutes: 0),
  566. initialAbsorptionTimeOverrun: 1.5,
  567. absorptionModel: PiecewiseLinearAbsorption(),
  568. adaptiveAbsorptionRateEnabled: false,
  569. adaptiveRateStandbyIntervalFraction: 0.2)
  570. XCTAssertEqual(statuses.count, 1)
  571. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 7008, accuracy: 1)
  572. let absorption = statuses[0].absorption!
  573. let unit = HKUnit.gram()
  574. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  575. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  576. from: inputICE[0].startDate,
  577. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  578. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  579. absorptionModel: PiecewiseLinearAbsorption(),
  580. delay: TimeInterval(minutes: 10),
  581. delta: TimeInterval(minutes: 5)
  582. )
  583. XCTAssertEqual(output.count, carbsOnBoard.count)
  584. for (expected, calculated) in zip(output, carbsOnBoard) {
  585. XCTAssertEqual(expected.startDate, calculated.startDate)
  586. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  587. }
  588. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  589. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  590. XCTAssertEqual(carbsOnBoard[20].quantity.doubleValue(for: unit), 5, accuracy: 1)
  591. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  592. }
  593. func testDynamicAbsorptionPiecewiseLinearFullyObserved() {
  594. let inputICE = loadICEInputFixture("ice_1_hour_input")
  595. let carbEntries = loadCarbEntryFixture()
  596. let output = loadCOBOutputFixture("ice_1_hour_output")
  597. let (carbRatios, insulinSensitivities) = loadSchedules()
  598. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  599. fast: TimeInterval(hours: 1),
  600. medium: TimeInterval(hours: 2),
  601. slow: TimeInterval(hours: 4)
  602. )
  603. let statuses = [carbEntries[0]].map(
  604. to: inputICE,
  605. carbRatio: carbRatios,
  606. insulinSensitivity: insulinSensitivities,
  607. absorptionTimeOverrun: 1.5,
  608. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  609. delay: TimeInterval(minutes: 0),
  610. initialAbsorptionTimeOverrun: 1.5,
  611. absorptionModel: PiecewiseLinearAbsorption(),
  612. adaptiveAbsorptionRateEnabled: false,
  613. adaptiveRateStandbyIntervalFraction: 0.2
  614. )
  615. XCTAssertEqual(statuses.count, 1)
  616. XCTAssertNotNil(statuses[0].absorption)
  617. // No remaining absorption
  618. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 0, accuracy: 1)
  619. let absorption = statuses[0].absorption!
  620. let unit = HKUnit.gram()
  621. // All should be absorbed
  622. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 44, accuracy: 1)
  623. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  624. from: inputICE[0].startDate,
  625. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  626. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  627. absorptionModel: PiecewiseLinearAbsorption(),
  628. delay: TimeInterval(minutes: 10),
  629. delta: TimeInterval(minutes: 5)
  630. )
  631. XCTAssertEqual(output.count, carbsOnBoard.count)
  632. for (expected, calculated) in zip(output, carbsOnBoard) {
  633. XCTAssertEqual(expected.startDate, calculated.startDate)
  634. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  635. }
  636. XCTAssertEqual(carbsOnBoard[0].quantity.doubleValue(for: unit), 0, accuracy: 1)
  637. XCTAssertEqual(carbsOnBoard[1].quantity.doubleValue(for: unit), 44, accuracy: 1)
  638. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  639. XCTAssertEqual(carbsOnBoard[10].quantity.doubleValue(for: unit), 21, accuracy: 1)
  640. XCTAssertEqual(carbsOnBoard[17].quantity.doubleValue(for: unit), 7, accuracy: 1)
  641. XCTAssertEqual(carbsOnBoard[18].quantity.doubleValue(for: unit), 4, accuracy: 1)
  642. XCTAssertEqual(carbsOnBoard[30].quantity.doubleValue(for: unit), 0, accuracy: 1)
  643. }
  644. func testDynamicAbsorptionPiecewiseLinearNeverFullyObserved() {
  645. let inputICE = loadICEInputFixture("ice_slow_absorption")
  646. let carbEntries = loadCarbEntryFixture()
  647. let output = loadCOBOutputFixture("ice_slow_absorption_piecewiselinear_output")
  648. let (carbRatios, insulinSensitivities) = loadSchedules()
  649. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  650. fast: TimeInterval(hours: 1),
  651. medium: TimeInterval(hours: 2),
  652. slow: TimeInterval(hours: 4)
  653. )
  654. let statuses = [carbEntries[1]].map(
  655. to: inputICE,
  656. carbRatio: carbRatios,
  657. insulinSensitivity: insulinSensitivities,
  658. absorptionTimeOverrun: 1.5,
  659. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  660. delay: TimeInterval(minutes: 0),
  661. initialAbsorptionTimeOverrun: 1.5,
  662. absorptionModel: PiecewiseLinearAbsorption(),
  663. adaptiveAbsorptionRateEnabled: false,
  664. adaptiveRateStandbyIntervalFraction: 0.2)
  665. XCTAssertEqual(statuses.count, 1)
  666. XCTAssertNotNil(statuses[0].absorption)
  667. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 6888, accuracy: 1)
  668. // Check 12 hours later
  669. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  670. from: inputICE[0].startDate,
  671. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 18)),
  672. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  673. absorptionModel: PiecewiseLinearAbsorption(),
  674. delay: TimeInterval(minutes: 10),
  675. delta: TimeInterval(minutes: 5)
  676. )
  677. let unit = HKUnit.gram()
  678. XCTAssertEqual(output.count, carbsOnBoard.count)
  679. for (expected, calculated) in zip(output, carbsOnBoard) {
  680. XCTAssertEqual(expected.startDate, calculated.startDate)
  681. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  682. }
  683. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  684. XCTAssertEqual(carbsOnBoard[5].quantity.doubleValue(for: unit), 30, accuracy: 1)
  685. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  686. }
  687. func testDynamicAbsorptionPiecewiseLinearAdaptiveRatePartiallyObserved() {
  688. let inputICE = loadICEInputFixture("ice_35_min_input")
  689. let carbEntries = loadCarbEntryFixture()
  690. let output = loadCOBOutputFixture("ice_35_min_partial_piecewiselinear_adaptiverate_output")
  691. let (carbRatios, insulinSensitivities) = loadSchedules()
  692. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  693. fast: TimeInterval(hours: 1),
  694. medium: TimeInterval(hours: 2),
  695. slow: TimeInterval(hours: 4)
  696. )
  697. let statuses = [carbEntries[0]].map(
  698. to: inputICE,
  699. carbRatio: carbRatios,
  700. insulinSensitivity: insulinSensitivities,
  701. absorptionTimeOverrun: 1.5,
  702. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  703. delay: TimeInterval(minutes: 0),
  704. initialAbsorptionTimeOverrun: 1.0,
  705. absorptionModel: PiecewiseLinearAbsorption(),
  706. adaptiveAbsorptionRateEnabled: true,
  707. adaptiveRateStandbyIntervalFraction: 0.2)
  708. XCTAssertEqual(statuses.count, 1)
  709. XCTAssertEqual(statuses[0].absorption!.estimatedTimeRemaining, 3326, accuracy: 1)
  710. let absorption = statuses[0].absorption!
  711. let unit = HKUnit.gram()
  712. XCTAssertEqual(absorption.observed.doubleValue(for: unit), 18, accuracy: Double(Float.ulpOfOne))
  713. let carbsOnBoard = statuses.dynamicCarbsOnBoard(
  714. from: inputICE[0].startDate,
  715. to: inputICE[0].startDate.addingTimeInterval(TimeInterval(hours: 6)),
  716. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  717. absorptionModel: PiecewiseLinearAbsorption(),
  718. delay: TimeInterval(minutes: 10),
  719. delta: TimeInterval(minutes: 5)
  720. )
  721. XCTAssertEqual(output.count, carbsOnBoard.count)
  722. for (expected, calculated) in zip(output, carbsOnBoard) {
  723. XCTAssertEqual(expected.startDate, calculated.startDate)
  724. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.gram()), calculated.quantity.doubleValue(for: HKUnit.gram()), accuracy: Double(Float.ulpOfOne))
  725. }
  726. XCTAssertEqual(carbsOnBoard.first!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  727. XCTAssertEqual(carbsOnBoard[2].quantity.doubleValue(for: unit), 44, accuracy: 1)
  728. XCTAssertEqual(carbsOnBoard[10].quantity.doubleValue(for: unit), 15, accuracy: 1)
  729. XCTAssertEqual(carbsOnBoard.last!.quantity.doubleValue(for: unit), 0, accuracy: 1)
  730. }
  731. func testDynamicAbsorptionMultipleEntries() {
  732. let inputICE = loadICEInputFixture("ice_35_min_input")
  733. let carbEntries = loadCarbEntryFixture()
  734. let (carbRatios, insulinSensitivities) = loadSchedules()
  735. let defaultAbsorptionTimes = CarbStore.DefaultAbsorptionTimes(
  736. fast: TimeInterval(hours: 1),
  737. medium: TimeInterval(hours: 2),
  738. slow: TimeInterval(hours: 4)
  739. )
  740. let statuses = carbEntries.map(
  741. to: inputICE,
  742. carbRatio: carbRatios,
  743. insulinSensitivity: insulinSensitivities,
  744. absorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  745. defaultAbsorptionTime: defaultAbsorptionTimes.medium,
  746. delay: TimeInterval(minutes: 0),
  747. initialAbsorptionTimeOverrun: defaultAbsorptionTimes.slow / defaultAbsorptionTimes.medium,
  748. absorptionModel: LinearAbsorption(),
  749. adaptiveAbsorptionRateEnabled: false,
  750. adaptiveRateStandbyIntervalFraction: 0.2
  751. )
  752. // Tuple structure: (observed absorption, estimated time remaining)
  753. let expected = [(16.193665456944906, 9100.254941363484), (1.806334543055097, 13532.959419333554) , (0, 14400)]
  754. XCTAssertEqual(expected.count, statuses.count)
  755. for (expected, calculated) in zip(expected, statuses) {
  756. XCTAssertEqual(expected.0, calculated.absorption?.observed.doubleValue(for: HKUnit.gram()))
  757. XCTAssertEqual(expected.1, calculated.absorption?.estimatedTimeRemaining)
  758. }
  759. }
  760. }