// // InsulinMathTests.swift // InsulinMathTests // // Created by Nathan Racklyeft on 1/27/16. // Copyright © 2016 Nathan Racklyeft. All rights reserved. // import XCTest import HealthKit @testable import LoopKit struct NewReservoirValue: ReservoirValue { let startDate: Date let unitVolume: Double } extension DoseUnit { var unit: HKUnit { switch self { case .units: return .internationalUnit() case .unitsPerHour: return HKUnit(from: "IU/hr") } } } class InsulinMathTests: XCTestCase { var fixtureDateformatter: DateFormatter! private let fixtureTimeZone = TimeZone(secondsFromGMT: -0 * 60 * 60)! private let model = WalshInsulinModel(actionDuration: TimeInterval(hours: 4)) private let insulinType: InsulinType = .novolog private let exponentialModel = ExponentialInsulinModel(actionDuration: TimeInterval(minutes: 360), peakActivityTime: TimeInterval(minutes: 75)) let insulinModelSettings = StaticInsulinModelProvider(ExponentialInsulinModelPreset.rapidActingAdult) let insulinModelDuration = ExponentialInsulinModelPreset.rapidActingAdult.effectDuration let walshModelSettings = StaticInsulinModelProvider( WalshInsulinModel(actionDuration: TimeInterval(hours: 4))) let walshModelDuration = WalshInsulinModel(actionDuration: TimeInterval(hours: 4)).effectDuration private func fixtureDate(_ input: String) -> Date { return fixtureDateformatter.date(from: input)! } override func setUp() { fixtureDateformatter = DateFormatter.descriptionFormatter fixtureDateformatter.timeZone = fixtureTimeZone } private func printInsulinValues(_ insulinValues: [InsulinValue]) { print("\n\n") print(String(data: try! JSONSerialization.data( withJSONObject: insulinValues.map({ (value) -> [String: Any] in return [ "date": ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone).string(from: value.startDate), "value": value.value, "unit": "U" ] }), options: .prettyPrinted), encoding: .utf8)!) print("\n\n") } private func printDoses(_ doses: [DoseEntry]) { print("\n\n") print(String(data: try! JSONSerialization.data( withJSONObject: doses.map({ (value) -> [String: Any] in var obj: [String: Any] = [ "type": value.type.pumpEventType.rawValue, "start_at": ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone).string(from: value.startDate), "end_at": ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone).string(from: value.endDate), "amount": value.value, "unit": value.unit.rawValue ] if let syncIdentifier = value.syncIdentifier { obj["raw"] = syncIdentifier } if let scheduledBasalRate = value.scheduledBasalRate { obj["scheduled"] = scheduledBasalRate.doubleValue(for: HKUnit(from: "IU/hr")) } return obj }), options: .prettyPrinted), encoding: .utf8)!) print("\n\n") } func loadReservoirFixture(_ resourceName: String) -> [NewReservoirValue] { let fixture: [JSONDictionary] = loadFixture(resourceName) let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) return fixture.map { return NewReservoirValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, unitVolume: $0["amount"] as! Double) } } func loadDoseFixture(_ resourceName: String, insulinType: InsulinType? = .novolog) -> [DoseEntry] { let fixture: [JSONDictionary] = loadFixture(resourceName) let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) return fixture.compactMap { guard let unit = DoseUnit(rawValue: $0["unit"] as! String), let pumpType = PumpEventType(rawValue: $0["type"] as! String), let type = DoseType(pumpEventType: pumpType) else { return nil } var dose = DoseEntry( type: type, startDate: dateFormatter.date(from: $0["start_at"] as! String)!, endDate: dateFormatter.date(from: $0["end_at"] as! String)!, value: $0["amount"] as! Double, unit: unit, deliveredUnits: $0["delivered"] as? Double, description: $0["description"] as? String, syncIdentifier: $0["raw"] as? String, insulinType: insulinType, automatic: $0["automatic"] as? Bool, manuallyEntered: $0["manuallyEntered"] as? Bool ?? false, isMutable: $0["isMutable"] as? Bool ?? false ) if let scheduled = $0["scheduled"] as? Double { dose.scheduledBasalRate = HKQuantity(unit: unit.unit, doubleValue: scheduled) } return dose } } func loadInsulinValueFixture(_ resourceName: String) -> [InsulinValue] { let fixture: [JSONDictionary] = loadFixture(resourceName) let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) return fixture.map { return InsulinValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, value: $0["value"] as! Double) } } func loadGlucoseEffectFixture(_ resourceName: String) -> [GlucoseEffect] { let fixture: [JSONDictionary] = loadFixture(resourceName) let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) return fixture.map { return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double)) } } func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { let fixture: [JSONDictionary] = loadFixture(resourceName) let items = fixture.map { return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) } return BasalRateSchedule(dailyItems: items, timeZone: fixtureTimeZone)! } var insulinSensitivitySchedule: InsulinSensitivitySchedule { return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 40.0)], timeZone: fixtureTimeZone)! } func testDoseEntriesFromReservoirValues() { let input = loadReservoirFixture("reservoir_history_with_rewind_and_prime_input") let output = loadDoseFixture("reservoir_history_with_rewind_and_prime_output").reversed() let doses = input.doseEntries XCTAssertEqual(output.count, doses.count) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.value, accuracy: Double(Float.ulpOfOne)) XCTAssertEqual(expected.unit, calculated.unit) } } func testContinuousReservoirValues() { var input = loadReservoirFixture("reservoir_history_with_rewind_and_prime_input") let within = TimeInterval(minutes: 30) let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) XCTAssertTrue(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T16:40:00")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: within)) // We don't assert whether it's "stale". XCTAssertTrue(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T16:40:00")!, to: dateFormatter.date(from: "2016-01-30T22:40:00")!, within: within)) XCTAssertTrue(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T16:40:00")!, to: Date(), within: within)) // The values must extend the startDate boundary XCTAssertFalse(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T15:00:00")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: within)) // (the boundary condition is GTE) XCTAssertTrue(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T16:00:42")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: within)) // Rises in reservoir volume taint the entire range XCTAssertFalse(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T15:55:00")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: within)) // Any values of 0 taint the entire range input.append(NewReservoirValue(startDate: dateFormatter.date(from: "2016-01-30T20:37:00")!, unitVolume: 0)) XCTAssertFalse(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T16:40:00")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: within)) // As long as the 0 is within the date interval bounds XCTAssertTrue(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T16:40:00")!, to: dateFormatter.date(from: "2016-01-30T19:40:00")!, within: within)) } func testNonContinuousReservoirValues() { let input = loadReservoirFixture("reservoir_history_with_continuity_holes") let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) XCTAssertTrue(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T18:30:00")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: .minutes(30))) XCTAssertFalse(input.isContinuous(from: dateFormatter.date(from: "2016-01-30T17:30:00")!, to: dateFormatter.date(from: "2016-01-30T20:40:00")!, within: .minutes(30))) } func testIOBFromSuspend() { let input = loadDoseFixture("suspend_dose") let reconciledOutput = loadDoseFixture("suspend_dose_reconciled") let normalizedOutput = loadDoseFixture("suspend_dose_reconciled_normalized") let iobOutput = loadInsulinValueFixture("suspend_dose_reconciled_normalized_iob") let basals = loadBasalRateScheduleFixture("basal") let reconciled = input.reconciled() XCTAssertEqual(reconciledOutput.count, reconciled.count) for (expected, calculated) in zip(reconciledOutput, reconciled) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.value) XCTAssertEqual(expected.unit, calculated.unit) } let normalized = reconciled.annotated(with: basals) XCTAssertEqual(normalizedOutput.count, normalized.count) for (expected, calculated) in zip(normalizedOutput, normalized) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.netBasalUnitsPerHour, accuracy: Double(Float.ulpOfOne)) } let iob = normalized.insulinOnBoard(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration) XCTAssertEqual(iobOutput.count, iob.count) for (expected, calculated) in zip(iobOutput, iob) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: Double(Float.ulpOfOne)) } } func testIOBFromDoses() { let input = loadDoseFixture("normalized_doses", insulinType: .novolog) let output = loadInsulinValueFixture("iob_from_doses_output") measure { _ = input.insulinOnBoard(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration) } let iob = input.insulinOnBoard(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration) XCTAssertEqual(output.count, iob.count) for (expected, calculated) in zip(output, iob) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: 0.5) } } func testIOBFromNoDoses() { let input: [DoseEntry] = [] let iob = input.insulinOnBoard(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration) XCTAssertEqual(0, iob.count) } func testInsulinOnBoardLimitsForExponentialModel() { let insulinModel = ExponentialInsulinModel(actionDuration: TimeInterval(minutes: 360), peakActivityTime: TimeInterval(minutes: 75), delay: TimeInterval(minutes: 0)) let childModel = ExponentialInsulinModel(actionDuration: TimeInterval(minutes: 360), peakActivityTime: TimeInterval(minutes: 65), delay: TimeInterval(minutes: 0)) XCTAssertEqual(1, insulinModel.percentEffectRemaining(at: .minutes(-1)), accuracy: 0.001) XCTAssertEqual(1, insulinModel.percentEffectRemaining(at: .minutes(0)), accuracy: 0.001) XCTAssertEqual(0, insulinModel.percentEffectRemaining(at: .minutes(360)), accuracy: 0.001) XCTAssertEqual(0, insulinModel.percentEffectRemaining(at: .minutes(361)), accuracy: 0.001) // Test random point XCTAssertEqual(0.5110493617156, insulinModel.percentEffectRemaining(at: .minutes(108)), accuracy: 0.001) // Test for child curve XCTAssertEqual(0.6002510111374046, childModel.percentEffectRemaining(at: .minutes(82)), accuracy: 0.001) } func testIOBFromDosesExponential() { let input = loadDoseFixture("normalized_doses", insulinType: .novolog) let output = loadInsulinValueFixture("iob_from_doses_exponential_output") measure { _ = input.insulinOnBoard(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration) } let iob = input.insulinOnBoard(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration) XCTAssertEqual(output.count, iob.count) for (expected, calculated) in zip(output, iob) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: 0.5) } } func testIOBFromBolusExponential() { let input = loadDoseFixture("bolus_dose", insulinType: .novolog) let output = loadInsulinValueFixture("iob_from_bolus_exponential_output") let iob = input.insulinOnBoard(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration) XCTAssertEqual(output.count, iob.count) for (expected, calculated) in zip(output, iob) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: Double(Float.ulpOfOne)) } } func testIOBFromBolus() { for hours in [2, 3, 4, 5, 5.2, 6, 7] as [Double] { let actionDuration = TimeInterval(hours: hours) let model = WalshInsulinModel(actionDuration: actionDuration) let insulinModelProvider = StaticInsulinModelProvider( model) let input = loadDoseFixture("bolus_dose", insulinType: .novolog) let output = loadInsulinValueFixture("iob_from_bolus_\(Int(actionDuration.minutes))min_output") let iob = input.insulinOnBoard(insulinModelProvider: insulinModelProvider, longestEffectDuration: model.effectDuration) XCTAssertEqual(output.count, iob.count) for (expected, calculated) in zip(output, iob) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: Double(Float.ulpOfOne)) } } } func testIOBFromDosesWithDifferentInsulinCurves() { let formatter = DateFormatter.descriptionFormatter let f = { (input) in return formatter.date(from: input)! } let output = loadInsulinValueFixture("iob_from_multiple_curves_output") let doses = [ DoseEntry(type: .basal, startDate: f("2018-05-15 14:42:36 +0000"), endDate: f("2018-05-16 14:42:36 +0000"), value: 0.84999999999999998, unit: .unitsPerHour, syncIdentifier: "7b02646a070f120e2200", scheduledBasalRate: nil), DoseEntry(type: .bolus, startDate: f("2018-05-15 14:44:46 +0000"), endDate: f("2018-05-15 14:44:46 +0000"), value: 0.9, unit: .units, syncIdentifier: "01004a004a006d006e22354312", scheduledBasalRate: nil), DoseEntry(type: .tempBasal, startDate: f("2018-05-15 14:42:36 +0000"), endDate: f("2018-05-15 14:42:36 +0000"), value: 0.0, unit: .unitsPerHour, syncIdentifier: "1600646a074f12", scheduledBasalRate: nil), DoseEntry(type: .tempBasal, startDate: f("2018-05-15 14:32:51 +0000"), endDate: f("2018-05-15 15:02:51 +0000"), value: 1.8999999999999999, unit: .unitsPerHour, syncIdentifier: "16017360074f12", scheduledBasalRate: nil), DoseEntry(type: .bolus, startDate: f("2018-05-15 14:52:51 +0000"), endDate: f("2018-05-15 15:52:51 +0000"), value: 0.9, unit: .units, syncIdentifier: "01004a004a006d006e22354312", scheduledBasalRate: nil), ] let iobWithoutModel = doses.insulinOnBoard(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration) let dosesWithModel = [ DoseEntry(type: .basal, startDate: f("2018-05-15 14:42:36 +0000"), endDate: f("2018-05-16 14:42:36 +0000"), value: 0.84999999999999998, unit: .unitsPerHour, syncIdentifier: "7b02646a070f120e2200", scheduledBasalRate: nil), DoseEntry(type: .bolus, startDate: f("2018-05-15 14:44:46 +0000"), endDate: f("2018-05-15 14:44:46 +0000"), value: 0.9, unit: .units, syncIdentifier: "01004a004a006d006e22354312", scheduledBasalRate: nil, insulinType: .fiasp), DoseEntry(type: .tempBasal, startDate: f("2018-05-15 14:42:36 +0000"), endDate: f("2018-05-15 14:42:36 +0000"), value: 0.0, unit: .unitsPerHour, syncIdentifier: "1600646a074f12", scheduledBasalRate: nil), DoseEntry(type: .tempBasal, startDate: f("2018-05-15 14:32:51 +0000"), endDate: f("2018-05-15 15:02:51 +0000"), value: 1.8999999999999999, unit: .unitsPerHour, syncIdentifier: "16017360074f12", scheduledBasalRate: nil), DoseEntry(type: .bolus, startDate: f("2018-05-15 14:52:51 +0000"), endDate: f("2018-05-15 15:52:51 +0000"), value: 0.9, unit: .units, syncIdentifier: "01004a004a006d006e22354312", scheduledBasalRate: nil, insulinType: .novolog), ] let insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: ExponentialInsulinModelPreset.rapidActingChild) let iobWithModel = dosesWithModel.insulinOnBoard(insulinModelProvider: insulinModelProvider, longestEffectDuration: ExponentialInsulinModelPreset.rapidActingChild.effectDuration) XCTAssertEqual(iobWithoutModel.count, iobWithModel.count) for (expected, calculated) in zip(output, iobWithModel) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: Double(Float.ulpOfOne)) } } func testIOBFromReservoirDoses() { let input = loadDoseFixture("normalized_reservoir_history_output") let output = loadInsulinValueFixture("iob_from_reservoir_output") measure { _ = input.insulinOnBoard(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration) } let iob = input.insulinOnBoard(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration) XCTAssertEqual(output.count, iob.count) for (expected, calculated) in zip(output, iob) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.value, calculated.value, accuracy: 0.4) } } func testNormalizeReservoirDoses() { let input = loadDoseFixture("reservoir_history_with_rewind_and_prime_output") let output = loadDoseFixture("normalized_reservoir_history_output") let basals = loadBasalRateScheduleFixture("basal") measure { _ = input.annotated(with: basals) } let doses = input.annotated(with: basals) XCTAssertEqual(output.count, doses.count) // Total delivery on split doses should add up to delivery from original doses XCTAssertEqual( input.map {$0.unitsInDeliverableIncrements}.reduce(0,+), doses.map {$0.unitsInDeliverableIncrements}.reduce(0,+), accuracy: Double(Float.ulpOfOne)) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.unitsPerHour, accuracy: Double(Float.ulpOfOne)) XCTAssertEqual(expected.scheduledBasalRate, calculated.scheduledBasalRate) } } func testNormalizeEdgeCaseDoses() { let input = loadDoseFixture("normalize_edge_case_doses_input") let output = loadDoseFixture("normalize_edge_case_doses_output") let basals = loadBasalRateScheduleFixture("basal") measure { _ = input.annotated(with: basals) } let doses = input.annotated(with: basals) XCTAssertEqual(output.count, doses.count) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.unit == .units ? calculated.netBasalUnits : calculated.netBasalUnitsPerHour) XCTAssertEqual(expected.unit, calculated.unit) } } func testNormalizeEdgeCaseDosesMutable() { let input = loadDoseFixture("normalize_edge_case_doses_mutable_input") let output = loadDoseFixture("normalize_edge_case_doses_mutable_output") let basals = loadBasalRateScheduleFixture("basal") measure { _ = input.annotated(with: basals) } let doses = input.annotated(with: basals) XCTAssertEqual(output.count, doses.count) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.unit == .units ? calculated.netBasalUnits : calculated.netBasalUnitsPerHour) XCTAssertEqual(expected.unit, calculated.unit) XCTAssertEqual(expected.isMutable, calculated.isMutable) XCTAssertEqual(expected.deliveredUnits, calculated.deliveredUnits) } } func testReconcileTempBasals() { // Fixture contains numerous overlapping temp basals, as well as a Suspend event interleaved with a temp basal let input = loadDoseFixture("reconcile_history_input") let output = loadDoseFixture("reconcile_history_output").sorted { $0.startDate < $1.startDate } let doses = input.reconciled().sorted { $0.startDate < $1.startDate } XCTAssertEqual(output.count, doses.count) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.value) XCTAssertEqual(expected.unit, calculated.unit) XCTAssertEqual(expected.syncIdentifier, calculated.syncIdentifier) XCTAssertEqual(expected.deliveredUnits, calculated.deliveredUnits) } } func testReconcileResumeBeforeRewind() { let input = loadDoseFixture("reconcile_resume_before_rewind_input") let output = loadDoseFixture("reconcile_resume_before_rewind_output") let doses = input.reconciled() XCTAssertEqual(output.count, doses.count) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.value) XCTAssertEqual(expected.unit, calculated.unit) XCTAssertEqual(expected.syncIdentifier, calculated.syncIdentifier) XCTAssertEqual(expected.deliveredUnits, calculated.deliveredUnits) } } func testGlucoseEffectFromBolus() { let input = loadDoseFixture("bolus_dose") let output = loadGlucoseEffectFixture("effect_from_bolus_output") let insulinSensitivitySchedule = self.insulinSensitivitySchedule measure { _ = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) } let effects = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) XCTAssertEqual(Float(output.count), Float(effects.count), accuracy: 1.0) for (expected, calculated) in zip(output, effects) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: 1.0) } } func testGlucoseEffectFromShortTempBasal() { let input = loadDoseFixture("short_basal_dose") let output = loadGlucoseEffectFixture("effect_from_bolus_output") let insulinSensitivitySchedule = self.insulinSensitivitySchedule measure { _ = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) } let effects = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) XCTAssertEqual(output.count, effects.count) for (expected, calculated) in zip(output, effects) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) } } func testGlucoseEffectFromTempBasal() { let input = loadDoseFixture("basal_dose") let output = loadGlucoseEffectFixture("effect_from_basal_output") let insulinSensitivitySchedule = self.insulinSensitivitySchedule measure { _ = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) } let effects = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) XCTAssertEqual(output.count, effects.count) for (expected, calculated) in zip(output, effects) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate)) } } func testGlucoseEffectFromTempBasalExponential() { let input = loadDoseFixture("basal_dose_with_delivered", insulinType: .novolog) let output = loadGlucoseEffectFixture("effect_from_basal_output_exponential") let effects = input.glucoseEffects(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration, insulinSensitivity: insulinSensitivitySchedule) XCTAssertEqual(output.count, effects.count) for (expected, calculated) in zip(output, effects) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate)) print(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter)) } } func testGlucoseEffectFromHistory() { let input = loadDoseFixture("normalized_doses") let output = loadGlucoseEffectFixture("effect_from_history_output") let insulinSensitivitySchedule = self.insulinSensitivitySchedule measure { _ = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) } let effects = input.glucoseEffects(insulinModelProvider: walshModelSettings, longestEffectDuration: walshModelDuration, insulinSensitivity: insulinSensitivitySchedule) XCTAssertEqual(output.count, effects.count) for (expected, calculated) in zip(output, effects) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 3.0) } } func testGlucoseEffectFromNoDoses() { let input: [DoseEntry] = [] let insulinSensitivitySchedule = self.insulinSensitivitySchedule let effects = input.glucoseEffects(insulinModelProvider: insulinModelSettings, longestEffectDuration: insulinModelDuration, insulinSensitivity: insulinSensitivitySchedule) XCTAssertEqual(0, effects.count) } func testTotalDelivery() { let input = loadDoseFixture("normalize_edge_case_doses_input") let output = input.totalDelivery XCTAssertEqual(18.8, output, accuracy: 0.01) } func testTrimContinuingDoses() { let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) let input = loadDoseFixture("normalized_doses").reversed() // Last temp ends at 2015-10-15T22:29:50 let endDate = dateFormatter.date(from: "2015-10-15T22:25:50")! let trimmed = input.map { $0.trimmed(to: endDate) } print(input, "\n\n\n") print(trimmed) XCTAssertEqual(endDate, trimmed.last!.endDate) XCTAssertEqual(input.count, trimmed.count) } func testTrimmedMaintainsMutability() { let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) let input = loadDoseFixture("normalized_doses").reversed() // Last temp ends at 2015-10-15T22:29:50 let endDate = dateFormatter.date(from: "2015-10-15T22:25:50")! let trimmed = input.map { $0.trimmed(to: endDate) } XCTAssertTrue(trimmed.last!.isMutable) } func testDosesOverlayBasalProfile() { let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: fixtureTimeZone) let input = loadDoseFixture("reconcile_history_output").sorted { $0.startDate < $1.startDate } let output = loadDoseFixture("doses_overlay_basal_profile_output") let basals = loadBasalRateScheduleFixture("basal") let doses = input.annotated(with: basals).overlayBasalSchedule( basals, // A start date before the first entry should generate a basal startingAt: dateFormatter.date(from: "2016-02-15T14:01:04")!, endingAt: Date(), insertingBasalEntries: true ) XCTAssertEqual(output.count, doses.count) XCTAssertEqual(doses.first?.startDate, dateFormatter.date(from: "2016-02-15T14:01:04")!) for (expected, calculated) in zip(output, doses) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.endDate, calculated.endDate) XCTAssertEqual(expected.value, calculated.value) XCTAssertEqual(expected.unit, calculated.unit) if let syncID = expected.syncIdentifier { XCTAssertEqual(syncID, calculated.syncIdentifier!) } } // Test trimming end let dosesTrimmedEnd = input[0.. DoseEntry in let startDate = self.fixtureDate("2018-07-16 03:49:00 +0000") let endDate = startDate.addingTimeInterval(TimeInterval(minutes: 5)) let tempBasalRate = 1.0 return DoseEntry( type: .tempBasal, startDate: startDate, endDate: endDate, value: tempBasalRate, unit: .unitsPerHour, deliveredUnits: deliveredUnits) } XCTAssertEqual(0.1, makeDose(nil).unitsInDeliverableIncrements, accuracy: .ulpOfOne) XCTAssertEqual(0.05, makeDose(0.05).unitsInDeliverableIncrements, accuracy: .ulpOfOne) } func testDoseEntryAnnotateShouldSplitDosesProportionally() { let startDate = self.fixtureDate("2018-07-16 11:59:00 +0000") let endDate = startDate.addingTimeInterval(TimeInterval(minutes: 5)) let tempBasalRate = 1.0 let dose = DoseEntry( type: .tempBasal, startDate: startDate, endDate: endDate, value: tempBasalRate, unit: .unitsPerHour, deliveredUnits: 0.1 ) let delivery = dose.unitsInDeliverableIncrements let basals = loadBasalRateScheduleFixture("basal") let splitDoses = [dose].annotated(with: basals) XCTAssertEqual(2, splitDoses.count) // A 5 minute dose starting one minute before midnight, split at midnight, means split should be 1/5, 4/5 XCTAssertEqual(delivery * 1.0/5.0, splitDoses[0].unitsInDeliverableIncrements, accuracy: .ulpOfOne) XCTAssertEqual(delivery * 4.0/5.0, splitDoses[1].unitsInDeliverableIncrements, accuracy: .ulpOfOne) } func testDoseEntryWithoutDeliveredUnitsShouldSplitDosesProportionally() { let startDate = self.fixtureDate("2018-07-16 11:59:00 +0000") let endDate = startDate.addingTimeInterval(TimeInterval(minutes: 5)) let tempBasalRate = 1.0 let dose = DoseEntry( type: .tempBasal, startDate: startDate, endDate: endDate, value: tempBasalRate, unit: .unitsPerHour, deliveredUnits: 0.05 ) let delivery = dose.unitsInDeliverableIncrements let basals = loadBasalRateScheduleFixture("basal") let splitDoses = [dose].annotated(with: basals) XCTAssertEqual(2, splitDoses.count) // A 5 minute dose starting one minute before midnight, split at midnight, means split should be 1/5, 4/5 XCTAssertEqual(delivery * 1.0/5.0, splitDoses[0].unitsInDeliverableIncrements, accuracy: .ulpOfOne) XCTAssertEqual(delivery * 4.0/5.0, splitDoses[1].unitsInDeliverableIncrements, accuracy: .ulpOfOne) } }