From 52be2830e39e1b2978ec44b6e3f8cf4f3e84fd80 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 31 Oct 2026 23:54:06 -0500 Subject: [PATCH 1/3] Take plain numbers in fixed units at the public API LoopKit currently depends on LoopAlgorithm for LoopQuantity and LoopUnit, so apps that want LoopKit without the algorithm (Trio) still have to build it. With this change LoopAlgorithm's public API uses Doubles in fixed units, so LoopKit can own its unit types and the two packages no longer need each other. - Glucose values are mg/dL (GlucoseValue.mgdl), carbs are grams (CarbEntry.grams), sensitivity is mg/dL per unit, targets and the suspend threshold are mg/dL, velocities are mg/dL per second. - LoopQuantity and LoopUnit are internal; the math is unchanged. - The LoopAlgorithm struct is renamed Algorithm, so LoopAlgorithm.X always names the module. - GlucoseSampleValue drops trendRate and condition, which the algorithm never read. - Helpers that apps don't call are internal. - LoopKit's tests of LoopMath and date rounding move here. --- Sources/LoopAlgorithm/AlgorithmInput.swift | 12 +- .../LoopAlgorithm/AlgorithmInputFixture.swift | 28 +- .../Carbs/AbsorbedCarbValue.swift | 30 +- Sources/LoopAlgorithm/Carbs/CarbEntry.swift | 8 + Sources/LoopAlgorithm/Carbs/CarbMath.swift | 42 +- Sources/LoopAlgorithm/Carbs/CarbStatus.swift | 4 +- Sources/LoopAlgorithm/Carbs/CarbValue.swift | 2 +- .../Carbs/FixtureCarbEntry.swift | 19 +- .../ExperimentalForecastAdjustments.swift | 4 +- .../Experiments/GuardAndRunwayOutput.swift | 2 +- .../Extensions/ClosedRange.swift | 2 +- .../Glucose/FixtureGlucoseSample.swift | 24 +- .../LoopAlgorithm/Glucose/GlucoseChange.swift | 26 +- .../Glucose/GlucoseCondition.swift | 2 +- .../LoopAlgorithm/Glucose/GlucoseEffect.swift | 14 +- .../Glucose/GlucoseEffectVelocity.swift | 33 +- .../LoopAlgorithm/Glucose/GlucoseMath.swift | 2 +- .../Glucose/GlucoseSampleValue.swift | 6 - .../LoopAlgorithm/Glucose/GlucoseValue.swift | 42 +- Sources/LoopAlgorithm/Insulin/DoseMath.swift | 34 +- .../LoopAlgorithm/Insulin/InsulinMath.swift | 41 +- .../LoopAlgorithm/Insulin/InsulinValue.swift | 2 +- .../Insulin/PrecomputedInsulinInput.swift | 9 +- Sources/LoopAlgorithm/LoopAlgorithm.swift | 151 +++-- Sources/LoopAlgorithm/LoopMath.swift | 2 +- .../LoopAlgorithm/LoopPredictionInput.swift | 24 +- Sources/LoopAlgorithm/LoopQuantity.swift | 20 +- Sources/LoopAlgorithm/LoopUnit.swift | 12 +- .../IntegralRetrospectiveCorrection.swift | 14 +- .../RetrospectiveCorrection.swift | 12 +- .../StandardRetrospectiveCorrection.swift | 8 +- Sources/LoopAlgorithm/SampleValue.swift | 3 +- Sources/LoopAlgorithmRunner/main.swift | 2 +- .../AlgorithmEmulationTests.swift | 4 +- .../AlgorithmExperimentsTests.swift | 70 +-- .../CorrectionDosingTests.swift | 100 ++-- .../DateRoundingTests.swift | 66 +++ ...lucose_from_effects_carb_effect_input.json | 467 ++++++++++++++++ .../glucose_from_effects_glucose_input.json | 6 + ...ose_from_effects_insulin_effect_input.json | 527 ++++++++++++++++++ ..._effects_momentum_blend_glucose_input.json | 20 + ...s_momentum_blend_insulin_effect_input.json | 37 ++ ...effects_momentum_blend_momentum_input.json | 7 + ...se_from_effects_momentum_blend_output.json | 1 + ...cose_from_effects_momentum_down_input.json | 37 ++ ...ose_from_effects_momentum_down_output.json | 1 + ...m_effects_momentum_flat_glucose_input.json | 6 + ...cose_from_effects_momentum_flat_input.json | 37 ++ ...ose_from_effects_momentum_flat_output.json | 1 + ...lucose_from_effects_momentum_up_input.json | 37 ++ ...ucose_from_effects_momentum_up_output.json | 1 + ...ucose_from_effects_no_momentum_output.json | 1 + ...cose_from_effects_non_zero_carb_input.json | 157 ++++++ ...e_from_effects_non_zero_glucose_input.json | 7 + ...e_from_effects_non_zero_insulin_input.json | 272 +++++++++ .../glucose_from_effects_non_zero_output.json | 277 +++++++++ ...ce_minus_carb_effect_with_gaps_output.json | 112 ++++ .../ice_minus_flat_carb_effect_output.json | 42 ++ .../LoopAlgorithmTests/GlucoseMathTests.swift | 8 +- .../LoopAlgorithmTests.swift | 78 +-- .../LoopMathEffectsTests.swift | 522 +++++++++++++++++ .../Mocks/LoopAlgorithmInputMock.swift | 6 +- .../PrecomputedInsulinInputTests.swift | 28 +- 63 files changed, 3218 insertions(+), 353 deletions(-) create mode 100644 Tests/LoopAlgorithmTests/DateRoundingTests.swift create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_carb_effect_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_glucose_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_insulin_effect_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_glucose_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_insulin_effect_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_momentum_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_glucose_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_no_momentum_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_carb_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_glucose_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_insulin_input.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/ice_minus_carb_effect_with_gaps_output.json create mode 100644 Tests/LoopAlgorithmTests/Fixtures/ice_minus_flat_carb_effect_output.json create mode 100644 Tests/LoopAlgorithmTests/LoopMathEffectsTests.swift diff --git a/Sources/LoopAlgorithm/AlgorithmInput.swift b/Sources/LoopAlgorithm/AlgorithmInput.swift index 42c2ded..dcf793d 100644 --- a/Sources/LoopAlgorithm/AlgorithmInput.swift +++ b/Sources/LoopAlgorithm/AlgorithmInput.swift @@ -19,10 +19,12 @@ public protocol AlgorithmInput { var doses: [InsulinDoseType] { get } var carbEntries: [CarbType] { get } var basal: [AbsoluteScheduleValue] { get } - var sensitivity: [AbsoluteScheduleValue] { get } + /// Insulin sensitivity in mg/dL per unit + var sensitivity: [AbsoluteScheduleValue] { get } var carbRatio: [AbsoluteScheduleValue] { get } var target: GlucoseRangeTimeline { get } - var suspendThreshold: LoopQuantity? { get } + /// Suspend threshold in mg/dL + var suspendThreshold: Double? { get } var maxBolus: Double { get } var maxActiveInsulinMultiplier: Double? { get } // Defaults to 2 (2x maxBolus) var maxBasalRate: Double { get } @@ -50,3 +52,9 @@ public extension AlgorithmInput { var experiments: Set { [] } var volatilityBaseline: Double? { nil } } + +extension AlgorithmInput { + var sensitivityQuantities: [AbsoluteScheduleValue] { sensitivity.mgdlQuantities } + var targetQuantities: QuantityRangeTimeline { target.quantities } + var suspendThresholdQuantity: LoopQuantity? { suspendThreshold.map(LoopQuantity.mgdl) } +} diff --git a/Sources/LoopAlgorithm/AlgorithmInputFixture.swift b/Sources/LoopAlgorithm/AlgorithmInputFixture.swift index 9f26e39..bcecc75 100644 --- a/Sources/LoopAlgorithm/AlgorithmInputFixture.swift +++ b/Sources/LoopAlgorithm/AlgorithmInputFixture.swift @@ -20,10 +20,10 @@ public struct AlgorithmInputFixture: AlgorithmInput { public var doses: [FixtureInsulinDose] public var carbEntries: [FixtureCarbEntry] public var basal: [AbsoluteScheduleValue] - public var sensitivity: [AbsoluteScheduleValue] + public var sensitivity: [AbsoluteScheduleValue] public var carbRatio: [AbsoluteScheduleValue] public var target: GlucoseRangeTimeline - public var suspendThreshold: LoopQuantity? + public var suspendThreshold: Double? public var maxBolus: Double public var maxActiveInsulinMultiplier: Double? public var maxBasalRate: Double @@ -63,10 +63,10 @@ public struct AlgorithmInputFixture: AlgorithmInput { doses: [FixtureInsulinDose], carbEntries: [FixtureCarbEntry], basal: [AbsoluteScheduleValue], - sensitivity: [AbsoluteScheduleValue], + sensitivity: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], target: GlucoseRangeTimeline, - suspendThreshold: LoopQuantity?, + suspendThreshold: Double?, maxBolus: Double, maxActiveInsulinMultiplier: Double? = nil, maxBasalRate: Double, @@ -114,19 +114,14 @@ extension AlgorithmInputFixture: Codable { self.doses = try container.decode([FixtureInsulinDose].self, forKey: .doses) self.carbEntries = try container.decode([FixtureCarbEntry].self, forKey: .carbEntries) self.basal = try container.decode([AbsoluteScheduleValue].self, forKey: .basal) - let sensitivityMgdl = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) - self.sensitivity = sensitivityMgdl.map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0.value))} + self.sensitivity = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) self.carbRatio = try container.decode([AbsoluteScheduleValue].self, forKey: .carbRatio) let targetMgdl = try container.decode([TargetEntry].self, forKey: .target) self.target = targetMgdl.map { - let lower = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0.lowerBound) - let upper = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0.upperBound) - let range = ClosedRange(uncheckedBounds: (lower: lower, upper: upper)) + let range = ClosedRange(uncheckedBounds: (lower: $0.lowerBound, upper: $0.upperBound)) return AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: range) } - if let suspendThresholdMgdl = try container.decodeIfPresent(Double.self, forKey: .suspendThreshold) { - self.suspendThreshold = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: suspendThresholdMgdl) - } + self.suspendThreshold = try container.decodeIfPresent(Double.self, forKey: .suspendThreshold) self.maxBolus = try container.decode(Double.self, forKey: .maxBolus) self.maxActiveInsulinMultiplier = try container.decodeIfPresent(Double.self, forKey: .maxActiveInsulinMultiplier) self.maxBasalRate = try container.decode(Double.self, forKey: .maxBasalRate) @@ -168,16 +163,13 @@ extension AlgorithmInputFixture: Codable { try container.encode(doses, forKey: .doses) try container.encode(carbEntries, forKey: .carbEntries) try container.encode(basal, forKey: .basal) - let sensitivityMgdl = sensitivity.map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.doubleValue(for: .milligramsPerDeciliter)) } - try container.encode(sensitivityMgdl, forKey: .sensitivity) + try container.encode(sensitivity, forKey: .sensitivity) try container.encode(carbRatio, forKey: .carbRatio) let targetMgdl = target.map { - let lower = $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter) - let upper = $0.value.upperBound.doubleValue(for: .milligramsPerDeciliter) - return TargetEntry(startDate: $0.startDate, endDate: $0.endDate, lowerBound: lower, upperBound: upper) + TargetEntry(startDate: $0.startDate, endDate: $0.endDate, lowerBound: $0.value.lowerBound, upperBound: $0.value.upperBound) } try container.encode(targetMgdl, forKey: .target) - try container.encode(suspendThreshold?.doubleValue(for: .milligramsPerDeciliter), forKey: .suspendThreshold) + try container.encode(suspendThreshold, forKey: .suspendThreshold) try container.encode(maxBolus, forKey: .maxBolus) try container.encode(maxActiveInsulinMultiplier, forKey: .maxActiveInsulinMultiplier) try container.encode(maxBasalRate, forKey: .maxBasalRate) diff --git a/Sources/LoopAlgorithm/Carbs/AbsorbedCarbValue.swift b/Sources/LoopAlgorithm/Carbs/AbsorbedCarbValue.swift index 6783b3c..ecf51fd 100644 --- a/Sources/LoopAlgorithm/Carbs/AbsorbedCarbValue.swift +++ b/Sources/LoopAlgorithm/Carbs/AbsorbedCarbValue.swift @@ -10,14 +10,19 @@ import Foundation /// A quantity of carbs absorbed over a given date interval public struct AbsorbedCarbValue: SampleValue { - /// The quantity of carbs absorbed - public let observed: LoopQuantity - /// The quantity of carbs absorbed, clamped to the original prediction - public let clamped: LoopQuantity - /// The quantity of carbs entered as eaten - public let total: LoopQuantity - /// The quantity of carbs expected to still absorb - public let remaining: LoopQuantity + let observed: LoopQuantity + let clamped: LoopQuantity + let total: LoopQuantity + let remaining: LoopQuantity + + /// Grams of carbs absorbed + public var observedGrams: Double { observed.doubleValue(for: .gram) } + /// Grams of carbs absorbed, clamped to the original prediction + public var clampedGrams: Double { clamped.doubleValue(for: .gram) } + /// Grams of carbs entered as eaten + public var totalGrams: Double { total.doubleValue(for: .gram) } + /// Grams of carbs expected to still absorb + public var remainingGrams: Double { remaining.doubleValue(for: .gram) } /// The dates over which absorption was observed public let observedDate: DateInterval @@ -37,7 +42,12 @@ public struct AbsorbedCarbValue: SampleValue { return estimatedTimeRemaining > 0 } - public var observedProgress: LoopQuantity { + /// Observed absorption as a fraction of the total entered + public var observedFraction: Double { + observedProgress.doubleValue(for: .percent) + } + + var observedProgress: LoopQuantity { let gram = LoopUnit.gram let totalGrams = total.doubleValue(for: gram) let percent = LoopUnit.percent @@ -54,7 +64,7 @@ public struct AbsorbedCarbValue: SampleValue { // MARK: SampleValue - public var quantity: LoopQuantity { + var quantity: LoopQuantity { return clamped } diff --git a/Sources/LoopAlgorithm/Carbs/CarbEntry.swift b/Sources/LoopAlgorithm/Carbs/CarbEntry.swift index 9c6d39a..66d5681 100644 --- a/Sources/LoopAlgorithm/Carbs/CarbEntry.swift +++ b/Sources/LoopAlgorithm/Carbs/CarbEntry.swift @@ -9,6 +9,14 @@ import Foundation public protocol CarbEntry: SampleValue { + /// The carbohydrate amount in grams + var grams: Double { get } var absorptionTime: TimeInterval? { get } } +extension CarbEntry { + var quantity: LoopQuantity { + LoopQuantity(unit: .gram, doubleValue: grams) + } +} + diff --git a/Sources/LoopAlgorithm/Carbs/CarbMath.swift b/Sources/LoopAlgorithm/Carbs/CarbMath.swift index 53b5779..30d891f 100644 --- a/Sources/LoopAlgorithm/Carbs/CarbMath.swift +++ b/Sources/LoopAlgorithm/Carbs/CarbMath.swift @@ -360,7 +360,7 @@ extension Collection { return values } - public func dynamicGlucoseEffects( + func dynamicGlucoseEffects( from start: Date? = nil, to end: Date? = nil, carbRatios: [AbsoluteScheduleValue], @@ -688,7 +688,7 @@ extension Collection where Element: CarbEntry { /// - defaultAbsorptionTime: The absorption time to use for unspecified carb entries /// - delay: The time to delay the dose effect /// - Returns: A new array of `CarbStatus` values describing the absorbed carb quantities - public func map( + func map( to effectVelocities: [GlucoseEffectVelocity], carbRatio: [AbsoluteScheduleValue], insulinSensitivity: [AbsoluteScheduleValue], @@ -790,3 +790,41 @@ extension Collection where Element: CarbEntry { return builders.map { $0.result } } } + +public extension Collection { + /// Carb effects from absorption status. Sensitivity is in mg/dL per unit. + func dynamicGlucoseEffects( + from start: Date? = nil, + to end: Date? = nil, + carbRatios: [AbsoluteScheduleValue], + insulinSensitivities: [AbsoluteScheduleValue], + defaultAbsorptionTime: TimeInterval = CarbMath.defaultAbsorptionTime, + absorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), + delay: TimeInterval = CarbMath.defaultEffectDelay, + delta: TimeInterval = GlucoseMath.defaultDelta + ) -> [GlucoseEffect] where Element == CarbStatus { + dynamicGlucoseEffects(from: start, to: end, carbRatios: carbRatios, insulinSensitivities: insulinSensitivities.mgdlQuantities, + defaultAbsorptionTime: defaultAbsorptionTime, absorptionModel: absorptionModel, delay: delay, delta: delta) + } +} + +public extension Collection where Element: CarbEntry { + /// Maps carb entries to observed absorption. Sensitivity is in mg/dL per unit. + func map( + to effectVelocities: [GlucoseEffectVelocity], + carbRatio: [AbsoluteScheduleValue], + insulinSensitivity: [AbsoluteScheduleValue], + absorptionTimeOverrun: Double = CarbMath.defaultAbsorptionTimeOverrun, + defaultAbsorptionTime: TimeInterval = CarbMath.defaultAbsorptionTime, + delay: TimeInterval = CarbMath.defaultEffectDelay, + initialAbsorptionTimeOverrun: Double = CarbMath.defaultAbsorptionTimeOverrun, + absorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), + adaptiveAbsorptionRateEnabled: Bool = false, + adaptiveRateStandbyIntervalFraction: Double = 0.2 + ) -> [CarbStatus] { + map(to: effectVelocities, carbRatio: carbRatio, insulinSensitivity: insulinSensitivity.mgdlQuantities, + absorptionTimeOverrun: absorptionTimeOverrun, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, + initialAbsorptionTimeOverrun: initialAbsorptionTimeOverrun, absorptionModel: absorptionModel, + adaptiveAbsorptionRateEnabled: adaptiveAbsorptionRateEnabled, adaptiveRateStandbyIntervalFraction: adaptiveRateStandbyIntervalFraction) + } +} diff --git a/Sources/LoopAlgorithm/Carbs/CarbStatus.swift b/Sources/LoopAlgorithm/Carbs/CarbStatus.swift index 219624b..6a3f215 100644 --- a/Sources/LoopAlgorithm/Carbs/CarbStatus.swift +++ b/Sources/LoopAlgorithm/Carbs/CarbStatus.swift @@ -22,8 +22,8 @@ public struct CarbStatus { // Masquerade as a carb entry, substituting AbsorbedCarbValue's interpretation of absorption time extension CarbStatus: SampleValue { - public var quantity: LoopQuantity { - return entry.quantity + public var grams: Double { + return entry.grams } public var startDate: Date { diff --git a/Sources/LoopAlgorithm/Carbs/CarbValue.swift b/Sources/LoopAlgorithm/Carbs/CarbValue.swift index 569cc9e..ce0fe7a 100644 --- a/Sources/LoopAlgorithm/Carbs/CarbValue.swift +++ b/Sources/LoopAlgorithm/Carbs/CarbValue.swift @@ -12,7 +12,7 @@ public struct CarbValue: SampleValue { public let endDate: Date public var value: Double - public var quantity: LoopQuantity { + var quantity: LoopQuantity { return LoopQuantity(unit: .gram, doubleValue: value) } diff --git a/Sources/LoopAlgorithm/Carbs/FixtureCarbEntry.swift b/Sources/LoopAlgorithm/Carbs/FixtureCarbEntry.swift index 451fff6..b7aefa9 100644 --- a/Sources/LoopAlgorithm/Carbs/FixtureCarbEntry.swift +++ b/Sources/LoopAlgorithm/Carbs/FixtureCarbEntry.swift @@ -10,20 +10,29 @@ import Foundation public struct FixtureCarbEntry: CarbEntry { public var absorptionTime: TimeInterval? public var startDate: Date - public var quantity: LoopQuantity + public var grams: Double public var foodType: String? public init( absorptionTime: TimeInterval? = nil, startDate: Date, - quantity: LoopQuantity, + grams: Double, foodType: String? = nil ) { self.absorptionTime = absorptionTime self.startDate = startDate - self.quantity = quantity + self.grams = grams self.foodType = foodType } + + init( + absorptionTime: TimeInterval? = nil, + startDate: Date, + quantity: LoopQuantity, + foodType: String? = nil + ) { + self.init(absorptionTime: absorptionTime, startDate: startDate, grams: quantity.doubleValue(for: .gram), foodType: foodType) + } } extension FixtureCarbEntry: Codable { @@ -32,7 +41,7 @@ extension FixtureCarbEntry: Codable { self.init( absorptionTime: try container.decodeIfPresent(TimeInterval.self, forKey: .absorptionTime), startDate: try container.decode(Date.self, forKey: .date), - quantity: LoopQuantity(unit: .gram, doubleValue: try container.decode(Double.self, forKey: .grams)) + grams: try container.decode(Double.self, forKey: .grams) ) } @@ -40,7 +49,7 @@ extension FixtureCarbEntry: Codable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(absorptionTime, forKey: .absorptionTime) try container.encode(startDate, forKey: .date) - try container.encode(quantity.doubleValue(for: .gram), forKey: .grams) + try container.encode(grams, forKey: .grams) } private enum CodingKeys: String, CodingKey { diff --git a/Sources/LoopAlgorithm/Experiments/ExperimentalForecastAdjustments.swift b/Sources/LoopAlgorithm/Experiments/ExperimentalForecastAdjustments.swift index 98e244c..671925c 100644 --- a/Sources/LoopAlgorithm/Experiments/ExperimentalForecastAdjustments.swift +++ b/Sources/LoopAlgorithm/Experiments/ExperimentalForecastAdjustments.swift @@ -22,7 +22,7 @@ extension ExperimentParameters.PostLowRiseCut { } } -extension LoopAlgorithm { +extension Algorithm { /// The volatility band: each predicted point τ minutes ahead is lowered by /// `gain × σ5 × (τ/5)^exponent` up to `horizon`, tapering to zero by `taperEnd`. @@ -164,7 +164,7 @@ extension LoopAlgorithm { } /// The target range lowered by `shift` mg/dL, floored at 70–80. - static func targetLowered(_ target: GlucoseRangeTimeline, by shift: Double) -> GlucoseRangeTimeline { + static func targetLowered(_ target: QuantityRangeTimeline, by shift: Double) -> QuantityRangeTimeline { let unit = LoopUnit.milligramsPerDeciliter return target.map { AbsoluteScheduleValue( diff --git a/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift b/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift index 77bcad8..90422a9 100644 --- a/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift +++ b/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift @@ -29,7 +29,7 @@ public struct GuardAndRunwayOutput: Equatable { /// `glucose` lowered by the band as the low guard saw it this run; unchanged when the band was off. /// `AlgorithmOutput.predictedGlucose` is the forecast without the band. public func guarded(_ glucose: [PredictedGlucoseValue]) -> [PredictedGlucoseValue] { - bandApplied ? LoopAlgorithm.volatilityBanded(glucose, band: band, volatility: volatility) : glucose + bandApplied ? Algorithm.volatilityBanded(glucose, band: band, volatility: volatility) : glucose } } diff --git a/Sources/LoopAlgorithm/Extensions/ClosedRange.swift b/Sources/LoopAlgorithm/Extensions/ClosedRange.swift index 77befd9..8d04f76 100644 --- a/Sources/LoopAlgorithm/Extensions/ClosedRange.swift +++ b/Sources/LoopAlgorithm/Extensions/ClosedRange.swift @@ -7,7 +7,7 @@ // extension ClosedRange where Bound == LoopQuantity { - public func averageValue(for unit: LoopUnit) -> Double { + func averageValue(for unit: LoopUnit) -> Double { let minValue = lowerBound.doubleValue(for: unit) let maxValue = upperBound.doubleValue(for: unit) return (maxValue + minValue) / 2 diff --git a/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift b/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift index 9d26171..6870e43 100644 --- a/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift +++ b/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift @@ -12,25 +12,35 @@ public struct FixtureGlucoseSample: GlucoseSampleValue, Equatable { public let provenanceIdentifier: String public let startDate: Date - public let quantity: LoopQuantity + public let mgdl: Double public let isDisplayOnly: Bool public let wasUserEntered: Bool - public var condition: GlucoseCondition? - public var trendRate: LoopQuantity? + var condition: GlucoseCondition? public init( provenanceIdentifier: String = Self.defaultProvenanceIdentifier, startDate: Date, - quantity: LoopQuantity, + mgdl: Double, isDisplayOnly: Bool = false, wasUserEntered: Bool = false ) { self.provenanceIdentifier = provenanceIdentifier self.startDate = startDate - self.quantity = quantity + self.mgdl = mgdl self.isDisplayOnly = isDisplayOnly self.wasUserEntered = wasUserEntered } + + init( + provenanceIdentifier: String = Self.defaultProvenanceIdentifier, + startDate: Date, + quantity: LoopQuantity, + isDisplayOnly: Bool = false, + wasUserEntered: Bool = false + ) { + self.init(provenanceIdentifier: provenanceIdentifier, startDate: startDate, mgdl: quantity.doubleValue(for: .milligramsPerDeciliter), + isDisplayOnly: isDisplayOnly, wasUserEntered: wasUserEntered) + } } extension FixtureGlucoseSample: Codable { @@ -43,7 +53,7 @@ extension FixtureGlucoseSample: Codable { self.init(provenanceIdentifier: provenanceIdentifier, startDate: try container.decode(Date.self, forKey: .date), - quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: try container.decode(Double.self, forKey: .value)), + mgdl: try container.decode(Double.self, forKey: .value), isDisplayOnly: isDisplayOnly, wasUserEntered: wasUserEntered ) @@ -55,7 +65,7 @@ extension FixtureGlucoseSample: Codable { try container.encode(provenanceIdentifier, forKey: .provenanceIdentifier) } try container.encode(startDate, forKey: .date) - try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .value) + try container.encode(mgdl, forKey: .value) if isDisplayOnly { try container.encode(isDisplayOnly, forKey: .isDisplayOnly) } diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseChange.swift b/Sources/LoopAlgorithm/Glucose/GlucoseChange.swift index 504ebd3..2cf84a5 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseChange.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseChange.swift @@ -11,12 +11,20 @@ import Foundation public struct GlucoseChange: SampleValue, Equatable { public var startDate: Date public var endDate: Date - public var quantity: LoopQuantity + public var mgdl: Double - public init(startDate: Date, endDate: Date, quantity: LoopQuantity) { + public init(startDate: Date, endDate: Date, mgdl: Double) { self.startDate = startDate self.endDate = endDate - self.quantity = quantity + self.mgdl = mgdl + } + + init(startDate: Date, endDate: Date, quantity: LoopQuantity) { + self.init(startDate: startDate, endDate: endDate, mgdl: quantity.doubleValue(for: .milligramsPerDeciliter)) + } + + var quantity: LoopQuantity { + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl) } } @@ -25,10 +33,7 @@ extension GlucoseChange { mutating public func append(_ effect: GlucoseEffect) { startDate = min(effect.startDate, startDate) endDate = max(effect.endDate, endDate) - quantity = LoopQuantity( - unit: .milligramsPerDeciliter, - doubleValue: quantity.doubleValue(for: .milligramsPerDeciliter) + effect.quantity.doubleValue(for: .milligramsPerDeciliter) - ) + mgdl += effect.mgdl } } @@ -37,17 +42,14 @@ extension GlucoseChange: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.startDate = try container.decode(Date.self, forKey: .startDate) self.endDate = try container.decode(Date.self, forKey: .endDate) - self.quantity = LoopQuantity( - unit: .milligramsPerDeciliter, - doubleValue: try container.decode(Double.self, forKey: .mgdl) - ) + self.mgdl = try container.decode(Double.self, forKey: .mgdl) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(startDate, forKey: .startDate) try container.encode(endDate, forKey: .endDate) - try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .mgdl) + try container.encode(mgdl, forKey: .mgdl) } private enum CodingKeys: String, CodingKey { diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift b/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift index 2ff6815..5b2de61 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift @@ -6,7 +6,7 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -public enum GlucoseCondition: String, Codable { +enum GlucoseCondition: String, Codable { case belowRange case aboveRange } diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseEffect.swift b/Sources/LoopAlgorithm/Glucose/GlucoseEffect.swift index 9572f80..c1790d6 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseEffect.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseEffect.swift @@ -10,11 +10,15 @@ import Foundation public struct GlucoseEffect: GlucoseValue, Equatable, Sendable { public let startDate: Date - public let quantity: LoopQuantity + public let mgdl: Double - public init(startDate: Date, quantity: LoopQuantity) { + public init(startDate: Date, mgdl: Double) { self.startDate = startDate - self.quantity = quantity + self.mgdl = mgdl + } + + init(startDate: Date, quantity: LoopQuantity) { + self.init(startDate: startDate, mgdl: quantity.doubleValue(for: .milligramsPerDeciliter)) } } @@ -22,13 +26,13 @@ extension GlucoseEffect: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.init(startDate: try container.decode(Date.self, forKey: .startDate), - quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: try container.decode(Double.self, forKey: .quantity))) + mgdl: try container.decode(Double.self, forKey: .quantity)) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(startDate, forKey: .startDate) - try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .quantity) + try container.encode(mgdl, forKey: .quantity) } private enum CodingKeys: String, CodingKey { diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseEffectVelocity.swift b/Sources/LoopAlgorithm/Glucose/GlucoseEffectVelocity.swift index 5df2542..af4fcc9 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseEffectVelocity.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseEffectVelocity.swift @@ -11,31 +11,31 @@ import Foundation public struct GlucoseEffectVelocity: SampleValue { public let startDate: Date public let endDate: Date - public let quantity: LoopQuantity + public let mgdlPerSecond: Double - public init(startDate: Date, endDate: Date, quantity: LoopQuantity) { + public init(startDate: Date, endDate: Date, mgdlPerSecond: Double) { self.startDate = startDate self.endDate = endDate - self.quantity = quantity + self.mgdlPerSecond = mgdlPerSecond + } + + init(startDate: Date, endDate: Date, quantity: LoopQuantity) { + self.init(startDate: startDate, endDate: endDate, mgdlPerSecond: quantity.doubleValue(for: GlucoseEffectVelocity.perSecondUnit)) + } + + var quantity: LoopQuantity { + LoopQuantity(unit: GlucoseEffectVelocity.perSecondUnit, doubleValue: mgdlPerSecond) } } extension GlucoseEffectVelocity { - public static let perSecondUnit = LoopUnit.milligramsPerDeciliterPerSecond + static let perSecondUnit = LoopUnit.milligramsPerDeciliterPerSecond /// The integration of the velocity span public var effect: GlucoseEffect { let duration = endDate.timeIntervalSince(startDate) - let velocityPerSecond = quantity.doubleValue(for: GlucoseEffectVelocity.perSecondUnit) - - return GlucoseEffect( - startDate: endDate, - quantity: LoopQuantity( - unit: .milligramsPerDeciliter, - doubleValue: velocityPerSecond * duration - ) - ) + return GlucoseEffect(startDate: endDate, mgdl: mgdlPerSecond * duration) } } @@ -44,17 +44,14 @@ extension GlucoseEffectVelocity: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.startDate = try container.decode(Date.self, forKey: .startDate) self.endDate = try container.decode(Date.self, forKey: .endDate) - self.quantity = LoopQuantity( - unit: GlucoseEffectVelocity.perSecondUnit, - doubleValue: try container.decode(Double.self, forKey: .mgdlPerSecond) - ) + self.mgdlPerSecond = try container.decode(Double.self, forKey: .mgdlPerSecond) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(startDate, forKey: .startDate) try container.encode(endDate, forKey: .endDate) - try container.encode(quantity.doubleValue(for: GlucoseEffectVelocity.perSecondUnit), forKey: .mgdlPerSecond) + try container.encode(mgdlPerSecond, forKey: .mgdlPerSecond) } private enum CodingKeys: String, CodingKey { diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift b/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift index 444e977..9ffa815 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift @@ -112,7 +112,7 @@ extension BidirectionalCollection where Element: GlucoseSampleValue, Index == In /// - delta: The time differential for the returned values /// - velocityMaximum: The limit on how fast the momentum effect can be. Defaults to 4 mg/dL/min based on physiological rates, if nil passed. /// - Returns: An array of glucose effects - public func linearMomentumEffect( + func linearMomentumEffect( duration: TimeInterval = GlucoseMath.momentumDuration, delta: TimeInterval = GlucoseMath.defaultDelta, velocityMaximum: LoopQuantity? = nil diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseSampleValue.swift b/Sources/LoopAlgorithm/Glucose/GlucoseSampleValue.swift index 3de0477..209482e 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseSampleValue.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseSampleValue.swift @@ -17,10 +17,4 @@ public protocol GlucoseSampleValue: GlucoseValue { /// Whether the glucose value is user entered, as opposed to a CGM value. var wasUserEntered: Bool { get } - - /// Any condition applied to the sample. - var condition: GlucoseCondition? { get } - - /// The trend rate of the sample. - var trendRate: LoopQuantity? { get } } diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseValue.swift b/Sources/LoopAlgorithm/Glucose/GlucoseValue.swift index de74f1a..6db0e93 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseValue.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseValue.swift @@ -8,23 +8,35 @@ import Foundation public protocol GlucoseValue: SampleValue { + /// The glucose value in mg/dL + var mgdl: Double { get } +} + +extension GlucoseValue { + var quantity: LoopQuantity { + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl) + } } public struct SimpleGlucoseValue: Equatable, GlucoseValue { public let startDate: Date public let endDate: Date - public let quantity: LoopQuantity + public let mgdl: Double - public init(startDate: Date, endDate: Date? = nil, quantity: LoopQuantity) { + public init(startDate: Date, endDate: Date? = nil, mgdl: Double) { self.startDate = startDate self.endDate = endDate ?? startDate - self.quantity = quantity + self.mgdl = mgdl + } + + init(startDate: Date, endDate: Date? = nil, quantity: LoopQuantity) { + self.init(startDate: startDate, endDate: endDate, mgdl: quantity.doubleValue(for: .milligramsPerDeciliter)) } public init(_ glucoseValue: GlucoseValue) { self.startDate = glucoseValue.startDate self.endDate = glucoseValue.endDate - self.quantity = glucoseValue.quantity + self.mgdl = glucoseValue.mgdl } } @@ -33,8 +45,8 @@ extension SimpleGlucoseValue: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.startDate = try container.decode(Date.self, forKey: .startDate) self.endDate = try container.decodeIfPresent(Date.self, forKey: .endDate) ?? self.startDate - self.quantity = LoopQuantity(unit: LoopUnit(from: try container.decode(String.self, forKey: .quantityUnit)), - doubleValue: try container.decode(Double.self, forKey: .quantity)) + self.mgdl = LoopQuantity(unit: LoopUnit(from: try container.decode(String.self, forKey: .quantityUnit)), + doubleValue: try container.decode(Double.self, forKey: .quantity)).doubleValue(for: .milligramsPerDeciliter) } public func encode(to encoder: Encoder) throws { @@ -43,7 +55,7 @@ extension SimpleGlucoseValue: Codable { if endDate != startDate { try container.encode(endDate, forKey: .endDate) } - try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .quantity) + try container.encode(mgdl, forKey: .quantity) try container.encode(LoopUnit.milligramsPerDeciliter.unitString, forKey: .quantityUnit) } @@ -57,11 +69,15 @@ extension SimpleGlucoseValue: Codable { public struct PredictedGlucoseValue: Equatable, GlucoseValue { public let startDate: Date - public let quantity: LoopQuantity + public let mgdl: Double - public init(startDate: Date, quantity: LoopQuantity) { + public init(startDate: Date, mgdl: Double) { self.startDate = startDate - self.quantity = quantity + self.mgdl = mgdl + } + + init(startDate: Date, quantity: LoopQuantity) { + self.init(startDate: startDate, mgdl: quantity.doubleValue(for: .milligramsPerDeciliter)) } } @@ -69,14 +85,14 @@ extension PredictedGlucoseValue: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.startDate = try container.decode(Date.self, forKey: .startDate) - self.quantity = LoopQuantity(unit: LoopUnit(from: try container.decode(String.self, forKey: .quantityUnit)), - doubleValue: try container.decode(Double.self, forKey: .quantity)) + self.mgdl = LoopQuantity(unit: LoopUnit(from: try container.decode(String.self, forKey: .quantityUnit)), + doubleValue: try container.decode(Double.self, forKey: .quantity)).doubleValue(for: .milligramsPerDeciliter) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(startDate, forKey: .startDate) - try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .quantity) + try container.encode(mgdl, forKey: .quantity) try container.encode(LoopUnit.milligramsPerDeciliter.unitString, forKey: .quantityUnit) } diff --git a/Sources/LoopAlgorithm/Insulin/DoseMath.swift b/Sources/LoopAlgorithm/Insulin/DoseMath.swift index 010fad9..465673a 100644 --- a/Sources/LoopAlgorithm/Insulin/DoseMath.swift +++ b/Sources/LoopAlgorithm/Insulin/DoseMath.swift @@ -8,7 +8,7 @@ import Foundation -public enum InsulinCorrection { +enum InsulinCorrection { case inRange case aboveRange(min: GlucoseValue, correcting: GlucoseValue, minTarget: LoopQuantity, units: Double) case entirelyBelowRange(min: GlucoseValue, minTarget: LoopQuantity, units: Double) @@ -149,7 +149,35 @@ private func targetGlucoseValue(percentEffectDuration: Double, minValue: Double, return minValue + slope * (percentEffectDuration - useMinValueUntilPercent) } -public typealias GlucoseRangeTimeline = [AbsoluteScheduleValue>] +/// Glucose target ranges over time, in mg/dL +public typealias GlucoseRangeTimeline = [AbsoluteScheduleValue>] + +typealias QuantityRangeTimeline = [AbsoluteScheduleValue>] + +extension Array where Element == AbsoluteScheduleValue> { + var quantities: QuantityRangeTimeline { + map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: LoopQuantity.mgdl($0.value.lowerBound)...LoopQuantity.mgdl($0.value.upperBound)) } + } +} + +extension Array where Element == AbsoluteScheduleValue> { + var mgdl: GlucoseRangeTimeline { + map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter)...$0.value.upperBound.doubleValue(for: .milligramsPerDeciliter)) } + } +} + +extension Array where Element == AbsoluteScheduleValue { + /// Treats the values as mg/dL (for insulin sensitivity, mg/dL per unit) + var mgdlQuantities: [AbsoluteScheduleValue] { + map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: LoopQuantity.mgdl($0.value)) } + } +} + +extension Array where Element == AbsoluteScheduleValue { + var mgdl: [AbsoluteScheduleValue] { + map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.doubleValue(for: .milligramsPerDeciliter)) } + } +} extension Array where Element: GlucoseValue { @@ -164,7 +192,7 @@ extension Array where Element: GlucoseValue { /// - model: The insulin effect model /// - Returns: A correction value in units, or nil if no correction needed func insulinCorrection( - to correctionRange: GlucoseRangeTimeline, + to correctionRange: QuantityRangeTimeline, at date: Date, suspendThreshold: LoopQuantity, insulinSensitivity: [AbsoluteScheduleValue], diff --git a/Sources/LoopAlgorithm/Insulin/InsulinMath.swift b/Sources/LoopAlgorithm/Insulin/InsulinMath.swift index a21a046..eb23af9 100644 --- a/Sources/LoopAlgorithm/Insulin/InsulinMath.swift +++ b/Sources/LoopAlgorithm/Insulin/InsulinMath.swift @@ -368,7 +368,7 @@ extension Collection where Element == BasalRelativeDose { /// - end: The latest date of effects to return. If nil is passed, it will be calculated from the last sample end date plus the longestEffectDuration. /// - delta: The interval between returned effects /// - Returns: An array of glucose effects for the duration of the doses - public func glucoseEffects( + func glucoseEffects( insulinSensitivityHistory: [AbsoluteScheduleValue], from start: Date? = nil, to end: Date? = nil, @@ -416,7 +416,7 @@ extension Collection where Element == BasalRelativeDose { /// - end: The latest date of effects to return /// - delta: The interval between returned effects /// - Returns: An array of glucose effects for the duration of the doses - public func glucoseEffectsMidAbsorptionISF( + func glucoseEffectsMidAbsorptionISF( longestEffectDuration: TimeInterval = InsulinMath.defaultInsulinActivityDuration, insulinSensitivityHistory: [AbsoluteScheduleValue], from start: Date? = nil, @@ -508,7 +508,7 @@ extension Collection where Element == BasalRelativeDose { /// - effectDates: The dates at which to calculate glucose effects /// - delta: The interval below which to consider doses as momentary /// - Returns: An array of glucose effects for the duration of the doses - public func glucoseEffects( + func glucoseEffects( longestEffectDuration: TimeInterval = InsulinMath.defaultInsulinActivityDuration, insulinSensitivityTimeline: [AbsoluteScheduleValue], effectDates: [Date], @@ -543,3 +543,38 @@ extension Collection where Element == BasalRelativeDose { return values } } + +public extension Collection where Element == BasalRelativeDose { + /// Glucose effects of the doses. Sensitivity is in mg/dL per unit. + func glucoseEffects( + insulinSensitivityHistory: [AbsoluteScheduleValue], + from start: Date? = nil, + to end: Date? = nil, + delta: TimeInterval = TimeInterval(/* minutes: */60 * 5) + ) -> [GlucoseEffect] { + glucoseEffects(insulinSensitivityHistory: insulinSensitivityHistory.mgdlQuantities, from: start, to: end, delta: delta) + } + + /// Glucose effects of the doses using mid-absorption ISF. Sensitivity is in mg/dL per unit. + func glucoseEffectsMidAbsorptionISF( + longestEffectDuration: TimeInterval = InsulinMath.defaultInsulinActivityDuration, + insulinSensitivityHistory: [AbsoluteScheduleValue], + from start: Date? = nil, + to end: Date? = nil, + delta: TimeInterval = TimeInterval(/* minutes: */60 * 5) + ) -> [GlucoseEffect] { + glucoseEffectsMidAbsorptionISF(longestEffectDuration: longestEffectDuration, insulinSensitivityHistory: insulinSensitivityHistory.mgdlQuantities, + from: start, to: end, delta: delta) + } + + /// Glucose effects of the doses at the given dates. Sensitivity is in mg/dL per unit. + func glucoseEffects( + longestEffectDuration: TimeInterval = InsulinMath.defaultInsulinActivityDuration, + insulinSensitivityTimeline: [AbsoluteScheduleValue], + effectDates: [Date], + delta: TimeInterval = TimeInterval(/* minutes: */60 * 5) + ) -> [GlucoseEffect] { + glucoseEffects(longestEffectDuration: longestEffectDuration, insulinSensitivityTimeline: insulinSensitivityTimeline.mgdlQuantities, + effectDates: effectDates, delta: delta) + } +} diff --git a/Sources/LoopAlgorithm/Insulin/InsulinValue.swift b/Sources/LoopAlgorithm/Insulin/InsulinValue.swift index dd34f40..871ed02 100644 --- a/Sources/LoopAlgorithm/Insulin/InsulinValue.swift +++ b/Sources/LoopAlgorithm/Insulin/InsulinValue.swift @@ -17,7 +17,7 @@ public struct InsulinValue: TimelineValue, Equatable { self.value = value } - public var quantity: LoopQuantity { + var quantity: LoopQuantity { LoopQuantity(unit: .internationalUnit, doubleValue: value) } } diff --git a/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift b/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift index 23894cf..ce58dc8 100644 --- a/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift +++ b/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift @@ -22,7 +22,7 @@ // │ let scaled = scaledSensitivity(sensitivity, by: multiplier) │ // │ let input = base.withEffects(sensitivity: scaled) // O(D×T) once │ // │ for t in sweepSteps { │ -// │ let result = LoopAlgorithm.generatePrediction( │ +// │ let result = Algorithm.generatePrediction( │ // │ start: t, ..., precomputedInsulin: input, ...) // no annotation │ // │ } │ // │ } │ @@ -61,7 +61,7 @@ private extension Array { /// let input = base.withEffects(sensitivity: scale(sensitivity, by: multiplier), /// from: sweepStart, to: sweepEnd + activityDuration) /// for t in sweepSteps { -/// let prediction = LoopAlgorithm.generatePrediction( +/// let prediction = Algorithm.generatePrediction( /// start: t, glucoseHistory: cgm[t], precomputedInsulin: input, ...) /// } /// } @@ -142,11 +142,12 @@ extension PrecomputedInsulinInput { /// - useMidAbsorptionISF: Use mid-absorption ISF computation. /// - Returns: A new `PrecomputedInsulinInput` with `insulinEffects` populated. public func withEffects( - sensitivity: [AbsoluteScheduleValue], + sensitivity: [AbsoluteScheduleValue], from: Date? = nil, to: Date? = nil, useMidAbsorptionISF: Bool = false ) -> PrecomputedInsulinInput { + let sensitivity = sensitivity.mgdlQuantities let effects: [GlucoseEffect] if useMidAbsorptionISF { effects = annotatedDoses.glucoseEffectsMidAbsorptionISF( @@ -198,7 +199,7 @@ extension PrecomputedInsulinInput { public static func build( doses: [DoseType], basal: [AbsoluteScheduleValue], - sensitivity: [AbsoluteScheduleValue]? = nil, + sensitivity: [AbsoluteScheduleValue]? = nil, effectsFrom: Date? = nil, effectsTo: Date? = nil, useMidAbsorptionISF: Bool = false diff --git a/Sources/LoopAlgorithm/LoopAlgorithm.swift b/Sources/LoopAlgorithm/LoopAlgorithm.swift index ba338bf..eeca9d0 100644 --- a/Sources/LoopAlgorithm/LoopAlgorithm.swift +++ b/Sources/LoopAlgorithm/LoopAlgorithm.swift @@ -27,7 +27,8 @@ public struct LoopAlgorithmEffects { public var momentum: [GlucoseEffect] public var insulinCounteraction: [GlucoseEffectVelocity] public var retrospectiveGlucoseDiscrepancies: [GlucoseChange] - public var totalRetrospectiveCorrectionEffect: LoopQuantity? + /// In mg/dL + public var totalRetrospectiveCorrectionEffect: Double? public init( insulin: [GlucoseEffect], @@ -37,7 +38,7 @@ public struct LoopAlgorithmEffects { momentum: [GlucoseEffect], insulinCounteraction: [GlucoseEffectVelocity], retrospectiveGlucoseDiscrepancies: [GlucoseChange], - totalRetrospectiveCorrectionEffect: LoopQuantity? = nil + totalRetrospectiveCorrectionEffect: Double? = nil ) { self.insulin = insulin self.carbs = carbs @@ -61,12 +62,7 @@ extension LoopAlgorithmEffects: Codable { self.insulinCounteraction = try container.decode([GlucoseEffectVelocity].self, forKey: .insulinCounteraction) self.retrospectiveGlucoseDiscrepancies = try container.decode([GlucoseChange].self, forKey: .retrospectiveGlucoseDiscrepancies) - if let totalRetrospectiveCorrectionEffectValue = try container.decodeIfPresent(Double.self, forKey: .totalRetrospectiveCorrectionEffect) { - self.totalRetrospectiveCorrectionEffect = LoopQuantity( - unit: .milligramsPerDeciliter, - doubleValue: totalRetrospectiveCorrectionEffectValue - ) - } + self.totalRetrospectiveCorrectionEffect = try container.decodeIfPresent(Double.self, forKey: .totalRetrospectiveCorrectionEffect) } public func encode(to encoder: Encoder) throws { @@ -78,12 +74,7 @@ extension LoopAlgorithmEffects: Codable { try container.encode(momentum, forKey: .momentum) try container.encode(insulinCounteraction, forKey: .insulinCounteraction) try container.encode(retrospectiveGlucoseDiscrepancies, forKey: .retrospectiveGlucoseDiscrepancies) - if let totalRetrospectiveCorrectionEffect { - try container.encode( - totalRetrospectiveCorrectionEffect.doubleValue(for: .milligramsPerDeciliter), - forKey: .totalRetrospectiveCorrectionEffect - ) - } + try container.encodeIfPresent(totalRetrospectiveCorrectionEffect, forKey: .totalRetrospectiveCorrectionEffect) } private enum CodingKeys: String, CodingKey { @@ -122,7 +113,7 @@ public struct LoopPrediction { public var activeCarbs: Double? } -public struct LoopAlgorithm { +public struct Algorithm { /// Percentage of recommended dose to apply as bolus when using automatic bolus dosing strategy static public let defaultBolusPartialApplicationFactor = 0.4 @@ -168,15 +159,17 @@ public struct LoopAlgorithm { /// - includingPositiveVelocityAndRC: If false, only net negative momentum and RC effects will used. /// - carbAbsorptionModel: A model conforming to CarbAbsorptionComputable that is used for computing carb absorption over time. /// - emulation: Options for reproducing earlier deployed algorithm behavior; the default emulates nothing. + /// - momentumVelocityMaximum: Limit on the momentum effect, in mg/dL per minute /// - Returns: A LoopPrediction struct containing the predicted glucose and the computed intermediate effects used to make the prediction - + /// + /// Sensitivity is in mg/dL per unit and target is in mg/dL. public static func generatePrediction( start: Date, glucoseHistory: [GlucoseType], doses: [InsulinDoseType], carbEntries: [CarbType], basal: [AbsoluteScheduleValue], - sensitivity: [AbsoluteScheduleValue], + sensitivity: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], target: GlucoseRangeTimeline = [], algorithmEffectsOptions: AlgorithmEffectsOptions = .all, @@ -185,6 +178,46 @@ public struct LoopAlgorithm { useMidAbsorptionISF: Bool = false, carbAbsorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), gradualTransitionsThreshold: Double? = 40.0, + momentumVelocityMaximum: Double? = nil, + emulation: AlgorithmEmulationOptions = AlgorithmEmulationOptions(), + experiments: Set = [] + ) -> LoopPrediction where CarbType: CarbEntry, GlucoseType: GlucoseSampleValue, InsulinDoseType: InsulinDose { + generatePrediction( + start: start, + glucoseHistory: glucoseHistory, + doses: doses, + carbEntries: carbEntries, + basal: basal, + sensitivity: sensitivity.mgdlQuantities, + carbRatio: carbRatio, + target: target.quantities, + algorithmEffectsOptions: algorithmEffectsOptions, + useIntegralRetrospectiveCorrection: useIntegralRetrospectiveCorrection, + includingPositiveVelocityAndRC: includingPositiveVelocityAndRC, + useMidAbsorptionISF: useMidAbsorptionISF, + carbAbsorptionModel: carbAbsorptionModel, + gradualTransitionsThreshold: gradualTransitionsThreshold, + momentumVelocityMaximum: momentumVelocityMaximum.map { LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) }, + emulation: emulation, + experiments: experiments + ) + } + + static func generatePrediction( + start: Date, + glucoseHistory: [GlucoseType], + doses: [InsulinDoseType], + carbEntries: [CarbType], + basal: [AbsoluteScheduleValue], + sensitivity: [AbsoluteScheduleValue], + carbRatio: [AbsoluteScheduleValue], + target: QuantityRangeTimeline = [], + algorithmEffectsOptions: AlgorithmEffectsOptions = .all, + useIntegralRetrospectiveCorrection: Bool = false, + includingPositiveVelocityAndRC: Bool = true, + useMidAbsorptionISF: Bool = false, + carbAbsorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), + gradualTransitionsThreshold: Double? = 40.0, momentumVelocityMaximum: LoopQuantity? = nil, emulation: AlgorithmEmulationOptions = AlgorithmEmulationOptions(), experiments: Set = [] @@ -379,7 +412,7 @@ public struct LoopAlgorithm { momentum: momentumEffects, insulinCounteraction: insulinCounteractionEffects, retrospectiveGlucoseDiscrepancies: retrospectiveGlucoseDiscrepanciesSummed, - totalRetrospectiveCorrectionEffect: totalRetrospectiveCorrectionEffect + totalRetrospectiveCorrectionEffect: totalRetrospectiveCorrectionEffect?.doubleValue(for: .milligramsPerDeciliter) ), dosesRelativeToBasal: dosesRelativeToBasal, activeInsulin: activeInsulin, @@ -429,7 +462,7 @@ public struct LoopAlgorithm { glucoseHistory: [GlucoseType], precomputedInsulin: PrecomputedInsulinInput, carbEntries: [CarbType], - sensitivity: [AbsoluteScheduleValue], + sensitivity: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], target: GlucoseRangeTimeline = [], scheduledBasalRate: Double? = nil, @@ -439,6 +472,44 @@ public struct LoopAlgorithm { useMidAbsorptionISF: Bool = false, carbAbsorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), gradualTransitionsThreshold: Double? = 40.0, + momentumVelocityMaximum: Double? = nil, + emulation: AlgorithmEmulationOptions = AlgorithmEmulationOptions() + ) -> LoopPrediction where CarbType: CarbEntry, GlucoseType: GlucoseSampleValue { + generatePrediction( + start: start, + glucoseHistory: glucoseHistory, + precomputedInsulin: precomputedInsulin, + carbEntries: carbEntries, + sensitivity: sensitivity.mgdlQuantities, + carbRatio: carbRatio, + target: target.quantities, + scheduledBasalRate: scheduledBasalRate, + algorithmEffectsOptions: algorithmEffectsOptions, + useIntegralRetrospectiveCorrection: useIntegralRetrospectiveCorrection, + includingPositiveVelocityAndRC: includingPositiveVelocityAndRC, + useMidAbsorptionISF: useMidAbsorptionISF, + carbAbsorptionModel: carbAbsorptionModel, + gradualTransitionsThreshold: gradualTransitionsThreshold, + momentumVelocityMaximum: momentumVelocityMaximum.map { LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) }, + emulation: emulation + ) + } + + static func generatePrediction( + start: Date, + glucoseHistory: [GlucoseType], + precomputedInsulin: PrecomputedInsulinInput, + carbEntries: [CarbType], + sensitivity: [AbsoluteScheduleValue], + carbRatio: [AbsoluteScheduleValue], + target: QuantityRangeTimeline = [], + scheduledBasalRate: Double? = nil, + algorithmEffectsOptions: AlgorithmEffectsOptions = .all, + useIntegralRetrospectiveCorrection: Bool = false, + includingPositiveVelocityAndRC: Bool = true, + useMidAbsorptionISF: Bool = false, + carbAbsorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), + gradualTransitionsThreshold: Double? = 40.0, momentumVelocityMaximum: LoopQuantity? = nil, emulation: AlgorithmEmulationOptions = AlgorithmEmulationOptions() ) -> LoopPrediction where CarbType: CarbEntry, GlucoseType: GlucoseSampleValue { @@ -592,7 +663,7 @@ public struct LoopAlgorithm { momentum: momentumEffects, insulinCounteraction: insulinCounteractionEffects, retrospectiveGlucoseDiscrepancies: retrospectiveGlucoseDiscrepanciesSummed, - totalRetrospectiveCorrectionEffect: totalRetrospectiveCorrectionEffect + totalRetrospectiveCorrectionEffect: totalRetrospectiveCorrectionEffect?.doubleValue(for: .milligramsPerDeciliter) ), dosesRelativeToBasal: dosesRelativeToBasal, activeInsulin: activeInsulin, @@ -609,9 +680,9 @@ public struct LoopAlgorithm { doses: input.doses, carbEntries: input.carbEntries, basal: input.basal, - sensitivity: input.sensitivity, + sensitivity: input.sensitivityQuantities, carbRatio: input.carbRatio, - target: input.target, + target: input.targetQuantities, algorithmEffectsOptions: input.algorithmEffectsOptions, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection, carbAbsorptionModel: input.carbAbsorptionModel.model, @@ -624,7 +695,7 @@ public struct LoopAlgorithm { /// current glucose and the lower correction target in effect now), else the input's constant. static func automaticBolusApplicationFactor(input: any AlgorithmInput, currentGlucose: some GlucoseSampleValue) -> Double { if input.experiments.contains(.glucoseBasedApplicationFactor), - let correctionRange = input.target.closestPrior(to: input.predictionStart)?.value + let correctionRange = input.targetQuantities.closestPrior(to: input.predictionStart)?.value { return GlucoseBasedApplicationFactor.factor( glucose: currentGlucose.quantity.doubleValue(for: .milligramsPerDeciliter), @@ -634,10 +705,10 @@ public struct LoopAlgorithm { } // Computes an amount of insulin to correct the given prediction - public static func insulinCorrection( + static func insulinCorrection( prediction: [PredictedGlucoseValue], at deliveryDate: Date, - target: GlucoseRangeTimeline, + target: QuantityRangeTimeline, suspendThreshold: LoopQuantity, sensitivity: [AbsoluteScheduleValue], insulinModel: InsulinModel @@ -651,7 +722,7 @@ public struct LoopAlgorithm { } // Computes a 30 minute temp basal dose to correct the given prediction - public static func recommendTempBasal( + static func recommendTempBasal( for correction: InsulinCorrection, neutralBasalRate: Double, activeInsulin: Double, @@ -683,7 +754,7 @@ public struct LoopAlgorithm { } // Computes a bolus or low-temp basal dose to correct the given prediction - public static func recommendAutomaticDose( + static func recommendAutomaticDose( for correction: InsulinCorrection, applicationFactor: Double, neutralBasalRate: Double, @@ -719,11 +790,11 @@ public struct LoopAlgorithm { } // Computes a manual bolus to correct the given prediction - public static func recommendManualBolus( + static func recommendManualBolus( for correction: InsulinCorrection, maxBolus: Double, currentGlucose: GlucoseSampleValue, - target: GlucoseRangeTimeline + target: QuantityRangeTimeline ) -> ManualBolusRecommendation { var bolus = correction.asManualBolus(maxBolus: maxBolus) @@ -786,10 +857,10 @@ public struct LoopAlgorithm { glucoseHistoryStart: glucoseStart, recommendationEffectInterval: recommendationEffectInterval ) - guard let sensitivityStartDate = input.sensitivity.first?.startDate, sensitivityStartDate <= neededISFInterval.start else { + guard let sensitivityStartDate = input.sensitivityQuantities.first?.startDate, sensitivityStartDate <= neededISFInterval.start else { throw AlgorithmError.sensitivityTimelineStartsTooLate } - guard let sensitivityEndDate = input.sensitivity.last?.endDate, sensitivityEndDate >= neededISFInterval.end else { + guard let sensitivityEndDate = input.sensitivityQuantities.last?.endDate, sensitivityEndDate >= neededISFInterval.end else { throw AlgorithmError.sensitivityTimelineEndsTooEarly } @@ -808,7 +879,7 @@ public struct LoopAlgorithm { throw AlgorithmError.basalTimelineIncomplete } - guard let suspendThreshold = input.suspendThreshold ?? input.target.closestPrior(to: input.predictionStart)?.value.lowerBound else { + guard let suspendThreshold = input.suspendThresholdQuantity ?? input.targetQuantities.closestPrior(to: input.predictionStart)?.value.lowerBound else { throw AlgorithmError.missingSuspendThreshold } @@ -821,7 +892,7 @@ public struct LoopAlgorithm { basal: input.basal, sensitivity: sensitivity, carbRatio: input.carbRatio, - target: input.target, + target: input.targetQuantities, algorithmEffectsOptions: .all, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection, includingPositiveVelocityAndRC: input.includePositiveVelocityAndRC, @@ -832,7 +903,7 @@ public struct LoopAlgorithm { experiments: input.experiments ) } - prediction = predict(sensitivity: input.sensitivity) + prediction = predict(sensitivity: input.sensitivityQuantities) // Experiment adjustments before dosing (no-ops when `experiments` is empty). let experimentParameters = ExperimentParameters( @@ -842,13 +913,13 @@ public struct LoopAlgorithm { // Slow retrospective correction: the level comes from the forecast at the scheduled ISF; the run // then uses ISF × multiplier throughout. - var sensitivity = input.sensitivity + var sensitivity = input.sensitivityQuantities if let slow = experimentParameters.slowRetrospectiveCorrection { let windowStart = input.predictionStart.addingTimeInterval(-slow.window) let carbEffects = prediction.effects.carbStatus.dynamicGlucoseEffects( from: windowStart.addingTimeInterval(-GlucoseMath.defaultDelta), carbRatios: input.carbRatio, - insulinSensitivities: input.sensitivity, + insulinSensitivities: input.sensitivityQuantities, absorptionModel: input.carbAbsorptionModel.model) let level = slowRetrospectiveCorrectionLevel( counteraction: prediction.effects.insulinCounteraction, carbEffects: carbEffects, @@ -856,7 +927,7 @@ public struct LoopAlgorithm { let multiplier = slowRetrospectiveCorrectionMultiplier(level: level, parameters: slow) slowRetrospectiveCorrection = SlowRetrospectiveCorrectionOutput(level: level, multiplier: multiplier) if multiplier != 1 { - sensitivity = sensitivityScaled(input.sensitivity, by: multiplier) + sensitivity = sensitivityScaled(input.sensitivityQuantities, by: multiplier) prediction = predict(sensitivity: sensitivity) } } @@ -866,7 +937,7 @@ public struct LoopAlgorithm { var runwayShift = 0.0 if let reserve = experimentParameters.runwayReserve, let volatility, let sensitivity = sensitivity.closestPrior(to: input.predictionStart)?.value, - let target = input.target.closestPrior(to: input.predictionStart)?.value + let target = input.targetQuantities.closestPrior(to: input.predictionStart)?.value { let unit = LoopUnit.milligramsPerDeciliter runwayShift = runwayAimShift( @@ -914,7 +985,7 @@ public struct LoopAlgorithm { // The runway shift lowers the aim of automatic dosing only; the manual bolus recommendation // keeps the user's target. let correctionTarget = runwayShift > 0 && input.recommendationType != .manualBolus - ? targetLowered(input.target, by: runwayShift) : input.target + ? targetLowered(input.targetQuantities, by: runwayShift) : input.targetQuantities var correction = insulinCorrection( prediction: prediction.glucose, at: input.predictionStart, @@ -940,7 +1011,7 @@ public struct LoopAlgorithm { for: correction, maxBolus: input.maxBolus, currentGlucose: latestGlucose, - target: input.target) + target: input.targetQuantities) result = .success(.init(manual: recommendation)) case .automaticBolus: let recommendation = recommendAutomaticDose( diff --git a/Sources/LoopAlgorithm/LoopMath.swift b/Sources/LoopAlgorithm/LoopMath.swift index cc09a30..cad7442 100644 --- a/Sources/LoopAlgorithm/LoopMath.swift +++ b/Sources/LoopAlgorithm/LoopMath.swift @@ -184,7 +184,7 @@ extension GlucoseValue { - returns: An array of glucose effects */ - public func decayEffect(atRate rate: LoopQuantity, for duration: TimeInterval, withDelta delta: TimeInterval = 5 * 60, useLegacyDecay: Bool = false) -> [GlucoseEffect] { + func decayEffect(atRate rate: LoopQuantity, for duration: TimeInterval, withDelta delta: TimeInterval = 5 * 60, useLegacyDecay: Bool = false) -> [GlucoseEffect] { guard let (startDate, endDate) = LoopMath.simulationDateRangeForSamples([self], duration: duration, delta: delta) else { return [] } diff --git a/Sources/LoopAlgorithm/LoopPredictionInput.swift b/Sources/LoopAlgorithm/LoopPredictionInput.swift index 9ac314c..f108899 100644 --- a/Sources/LoopAlgorithm/LoopPredictionInput.swift +++ b/Sources/LoopAlgorithm/LoopPredictionInput.swift @@ -22,7 +22,8 @@ public struct LoopPredictionInput] // Expected time range coverage: t-16h to t (eventually with mid-absorption isf changes, it will be t-10h to t) - public var sensitivity: [AbsoluteScheduleValue] + // Insulin sensitivity in mg/dL per unit + public var sensitivity: [AbsoluteScheduleValue] // Expected time range coverage: t-10h to t+6h public var carbRatio: [AbsoluteScheduleValue] @@ -48,7 +49,7 @@ public struct LoopPredictionInput], - sensitivity: [AbsoluteScheduleValue], + sensitivity: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], target: GlucoseRangeTimeline = [], algorithmEffectsOptions: AlgorithmEffectsOptions, @@ -76,6 +77,11 @@ public struct LoopPredictionInput] { sensitivity.mgdlQuantities } + var targetQuantities: QuantityRangeTimeline { target.quantities } +} + extension LoopPredictionInput: Codable where CarbType == FixtureCarbEntry, GlucoseType == FixtureGlucoseSample, InsulinDoseType == FixtureInsulinDose { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -84,14 +90,11 @@ extension LoopPredictionInput: Codable where CarbType == FixtureCarbEntry, Gluco self.doses = try container.decode([FixtureInsulinDose].self, forKey: .doses) self.carbEntries = try container.decode([FixtureCarbEntry].self, forKey: .carbEntries) self.basal = try container.decode([AbsoluteScheduleValue].self, forKey: .basal) - let sensitivityMgdl = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) - self.sensitivity = sensitivityMgdl.map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0.value))} + self.sensitivity = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) self.carbRatio = try container.decode([AbsoluteScheduleValue].self, forKey: .carbRatio) if let targetMgdl = try container.decodeIfPresent([TargetEntry].self, forKey: .target) { self.target = targetMgdl.map { - let lower = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0.lowerBound) - let upper = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0.upperBound) - return AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: lower...upper) + AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.lowerBound...$0.upperBound) } } else { self.target = [] @@ -114,16 +117,15 @@ extension LoopPredictionInput: Codable where CarbType == FixtureCarbEntry, Gluco try container.encode(doses, forKey: .doses) try container.encode(carbEntries, forKey: .carbEntries) try container.encode(basal, forKey: .basal) - let sensitivityMgdl = sensitivity.map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.doubleValue(for: .milligramsPerDeciliter)) } - try container.encode(sensitivityMgdl, forKey: .sensitivity) + try container.encode(sensitivity, forKey: .sensitivity) try container.encode(carbRatio, forKey: .carbRatio) if !target.isEmpty { let targetMgdl = target.map { TargetEntry( startDate: $0.startDate, endDate: $0.endDate, - lowerBound: $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter), - upperBound: $0.value.upperBound.doubleValue(for: .milligramsPerDeciliter)) + lowerBound: $0.value.lowerBound, + upperBound: $0.value.upperBound) } try container.encode(targetMgdl, forKey: .target) } diff --git a/Sources/LoopAlgorithm/LoopQuantity.swift b/Sources/LoopAlgorithm/LoopQuantity.swift index 34a063b..504854e 100644 --- a/Sources/LoopAlgorithm/LoopQuantity.swift +++ b/Sources/LoopAlgorithm/LoopQuantity.swift @@ -7,17 +7,21 @@ import Foundation -public struct LoopQuantity: Hashable, Equatable, Comparable, Sendable { +struct LoopQuantity: Hashable, Equatable, Comparable, Sendable { - public let unit: LoopUnit + let unit: LoopUnit private let value: Double - public init(unit: LoopUnit, doubleValue value: Double) { + init(unit: LoopUnit, doubleValue value: Double) { self.unit = unit self.value = value } + + static func mgdl(_ value: Double) -> LoopQuantity { + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: value) + } - public func `is`(compatibleWith unit: LoopUnit) -> Bool { + func `is`(compatibleWith unit: LoopUnit) -> Bool { self.unit.conversionFactor(toUnit: unit) != nil } @@ -26,7 +30,7 @@ public struct LoopQuantity: Hashable, Equatable, Comparable, Sendable { @abstract Returns the quantity value converted to the given unit. @discussion Throws an exception if the receiver's value cannot be converted to one of the requested unit. */ - public func doubleValue(for unit: LoopUnit) -> Double { + func doubleValue(for unit: LoopUnit) -> Double { guard let conversionFactor = self.unit.conversionFactor(toUnit: unit) else { fatalError("Conversion Error: \(self.unit.unitString) is not compatible with \(unit.unitString).") } @@ -44,7 +48,7 @@ public struct LoopQuantity: Hashable, Equatable, Comparable, Sendable { less than a given quantity. @discussion Throws an exception if the unit of the given quantity is not compatible with the receiver's unit. */ - public func compare(_ quantity: LoopQuantity) -> ComparisonResult { + func compare(_ quantity: LoopQuantity) -> ComparisonResult { if value == quantity.doubleValue(for: unit) { return .orderedSame } else if value > quantity.doubleValue(for: unit) { @@ -54,11 +58,11 @@ public struct LoopQuantity: Hashable, Equatable, Comparable, Sendable { } } - public static func <(lhs: LoopQuantity, rhs: LoopQuantity) -> Bool { + static func <(lhs: LoopQuantity, rhs: LoopQuantity) -> Bool { return lhs.compare(rhs) == .orderedAscending } - public static func == (lhs: LoopQuantity, rhs: LoopQuantity) -> Bool { + static func == (lhs: LoopQuantity, rhs: LoopQuantity) -> Bool { guard lhs.unit != rhs.unit else { return lhs.value == rhs.value } diff --git a/Sources/LoopAlgorithm/LoopUnit.swift b/Sources/LoopAlgorithm/LoopUnit.swift index 370334b..6a8c44a 100644 --- a/Sources/LoopAlgorithm/LoopUnit.swift +++ b/Sources/LoopAlgorithm/LoopUnit.swift @@ -10,7 +10,7 @@ import Foundation let UnitMolarMassBloodGlucose = 180.1558800000541 let UnitMolarMassBloodGlucoseDivisible = UnitMolarMassBloodGlucose / 10 -public enum LoopUnit: Sendable, CaseIterable { +enum LoopUnit: Sendable, CaseIterable { case gram case gramsPerUnit case internationalUnit @@ -28,11 +28,11 @@ public enum LoopUnit: Sendable, CaseIterable { case minute case second - public init(from string: String) { + init(from string: String) { self = LoopUnit.allCases.first(where: { $0.unitString == string }) ?? .gram } - public func conversionFactor(toUnit: LoopUnit) -> Double? { + func conversionFactor(toUnit: LoopUnit) -> Double? { switch (self, toUnit) { case (.gram, .gram), (.gramsPerUnit, .gramsPerUnit), @@ -108,7 +108,7 @@ public enum LoopUnit: Sendable, CaseIterable { } } - public var unitString: String { + var unitString: String { switch self { case .gram: return "g" @@ -145,7 +145,7 @@ public enum LoopUnit: Sendable, CaseIterable { } } - public var localizedShortUnitString: String { + var localizedShortUnitString: String { switch self { case .millimolesPerLiter: return NSLocalizedString("mmol/L", comment: "The short unit display string for millimoles of glucose per liter") case .milligramsPerDeciliter: return NSLocalizedString("mg/dL", comment: "The short unit display string for milligrams of glucose per decilter") @@ -155,7 +155,7 @@ public enum LoopUnit: Sendable, CaseIterable { } } - public func unitDivided(by unit: LoopUnit) -> LoopUnit { + func unitDivided(by unit: LoopUnit) -> LoopUnit { switch self { case .milligramsPerDeciliter: switch unit { diff --git a/Sources/LoopAlgorithm/RetrospectiveCorrection/IntegralRetrospectiveCorrection.swift b/Sources/LoopAlgorithm/RetrospectiveCorrection/IntegralRetrospectiveCorrection.swift index 3e7b95f..2122505 100644 --- a/Sources/LoopAlgorithm/RetrospectiveCorrection/IntegralRetrospectiveCorrection.swift +++ b/Sources/LoopAlgorithm/RetrospectiveCorrection/IntegralRetrospectiveCorrection.swift @@ -14,14 +14,14 @@ import Foundation In the above summary, "discrepancy" is a difference between the actual glucose and the model predicted glucose over retrospective correction grouping interval (set to 30 min in LoopSettings), whereas "past discrepancies" refers to a timeline of discrepancies computed over retrospective correction integration interval (set to 180 min in Loop Settings). */ -public class IntegralRetrospectiveCorrection: RetrospectiveCorrection { - public static let retrospectionInterval = TimeInterval(minutes: 180) +class IntegralRetrospectiveCorrection: RetrospectiveCorrection { + static let retrospectionInterval = TimeInterval(minutes: 180) /// RetrospectiveCorrection protocol variables /// Standard effect duration let effectDuration: TimeInterval /// Overall retrospective correction effect - public var totalGlucoseCorrectionEffect: LoopQuantity? + var totalGlucoseCorrectionEffect: LoopQuantity? /** Integral retrospective correction parameters: @@ -52,11 +52,11 @@ public class IntegralRetrospectiveCorrection: RetrospectiveCorrection { /// unmodeled glucose velocity (~4 mg/dL/min); unlike a bound scaled by ISF/basal/target, /// it does not depend on the user's dosing settings — the plausible velocity of unmodeled /// physiology is the same regardless of a person's insulin needs or target range. - public static let defaultMaxCorrectionVelocity = LoopQuantity( + static let defaultMaxCorrectionVelocity = LoopQuantity( unit: .milligramsPerDeciliterPerSecond, doubleValue: 4.0 / 60.0) /// Ceiling applied to the correction rate (see `defaultMaxCorrectionVelocity`); nil disables it. - public let maxCorrectionVelocity: LoopQuantity? + let maxCorrectionVelocity: LoopQuantity? /// Emulate pre-#33 (deployed Loop) step-by-step decay for the RC effect timeline. let useLegacyDecay: Bool @@ -78,7 +78,7 @@ public class IntegralRetrospectiveCorrection: RetrospectiveCorrection { /// the post-low rise-cut (see `ExperimentOption`). 1.0 is standard IRC. let riseScale: Double - public init(effectDuration: TimeInterval, + init(effectDuration: TimeInterval, maxCorrectionVelocity: LoopQuantity? = IntegralRetrospectiveCorrection.defaultMaxCorrectionVelocity, useLegacyDecay: Bool = false, riseScale: Double = 1.0) { @@ -98,7 +98,7 @@ public class IntegralRetrospectiveCorrection: RetrospectiveCorrection { - Returns: - totalRetrospectiveCorrection: Overall glucose effect */ - public func computeEffect( + func computeEffect( startingAt startingGlucose: GlucoseValue, retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?, recencyInterval: TimeInterval, diff --git a/Sources/LoopAlgorithm/RetrospectiveCorrection/RetrospectiveCorrection.swift b/Sources/LoopAlgorithm/RetrospectiveCorrection/RetrospectiveCorrection.swift index d13f560..7cd481f 100644 --- a/Sources/LoopAlgorithm/RetrospectiveCorrection/RetrospectiveCorrection.swift +++ b/Sources/LoopAlgorithm/RetrospectiveCorrection/RetrospectiveCorrection.swift @@ -12,15 +12,15 @@ import Foundation /// LoopKit's IntegralRetrospectiveCorrection applies: the wound-up integral term /// is bounded by an ISF×basal-scaled, correction-range-relative window. All three /// values are required for the bound to be well defined, so they travel together. -public struct IntegralRCClampSettings { +struct IntegralRCClampSettings { /// Insulin sensitivity at decision time - public var insulinSensitivity: LoopQuantity + var insulinSensitivity: LoopQuantity /// Scheduled basal rate at decision time, in U/hr - public var basalRate: Double + var basalRate: Double /// Correction range at decision time - public var correctionRange: ClosedRange + var correctionRange: ClosedRange - public init(insulinSensitivity: LoopQuantity, basalRate: Double, correctionRange: ClosedRange) { + init(insulinSensitivity: LoopQuantity, basalRate: Double, correctionRange: ClosedRange) { self.insulinSensitivity = insulinSensitivity self.basalRate = basalRate self.correctionRange = correctionRange @@ -28,7 +28,7 @@ public struct IntegralRCClampSettings { } /// Derives a continued glucose effect from recent prediction discrepancies -public protocol RetrospectiveCorrection { +protocol RetrospectiveCorrection { /// Overall retrospective correction effect var totalGlucoseCorrectionEffect: LoopQuantity? { get } diff --git a/Sources/LoopAlgorithm/RetrospectiveCorrection/StandardRetrospectiveCorrection.swift b/Sources/LoopAlgorithm/RetrospectiveCorrection/StandardRetrospectiveCorrection.swift index 6d9220f..7178b4f 100644 --- a/Sources/LoopAlgorithm/RetrospectiveCorrection/StandardRetrospectiveCorrection.swift +++ b/Sources/LoopAlgorithm/RetrospectiveCorrection/StandardRetrospectiveCorrection.swift @@ -13,14 +13,14 @@ import Foundation In the above summary, "discrepancy" is a difference between the actual glucose and the model predicted glucose over retrospective correction grouping interval (set to 30 min in LoopSettings) */ -public class StandardRetrospectiveCorrection: RetrospectiveCorrection { +class StandardRetrospectiveCorrection: RetrospectiveCorrection { /// RetrospectiveCorrection protocol variables /// Standard effect duration let effectDuration: TimeInterval /// Emulate pre-#33 (deployed Loop) step-by-step decay for the RC effect timeline. let useLegacyDecay: Bool /// Overall retrospective correction effect - public var totalGlucoseCorrectionEffect: LoopQuantity? + var totalGlucoseCorrectionEffect: LoopQuantity? /// All math is performed with glucose expressed in mg/dL private let unit = LoopUnit.milligramsPerDeciliter @@ -29,13 +29,13 @@ public class StandardRetrospectiveCorrection: RetrospectiveCorrection { /// `ExperimentOption`). 1.0 is standard RC. let riseScale: Double - public init(effectDuration: TimeInterval, useLegacyDecay: Bool = false, riseScale: Double = 1.0) { + init(effectDuration: TimeInterval, useLegacyDecay: Bool = false, riseScale: Double = 1.0) { self.effectDuration = effectDuration self.useLegacyDecay = useLegacyDecay self.riseScale = riseScale } - public func computeEffect( + func computeEffect( startingAt startingGlucose: GlucoseValue, retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?, recencyInterval: TimeInterval, diff --git a/Sources/LoopAlgorithm/SampleValue.swift b/Sources/LoopAlgorithm/SampleValue.swift index 28929e2..775a80e 100644 --- a/Sources/LoopAlgorithm/SampleValue.swift +++ b/Sources/LoopAlgorithm/SampleValue.swift @@ -21,8 +21,9 @@ public extension TimelineValue { } +/// A timeline value with an amount. Each refinement names its amount in a fixed unit, such as +/// `GlucoseValue.mgdl` or `CarbEntry.grams`. public protocol SampleValue: TimelineValue { - var quantity: LoopQuantity { get } } diff --git a/Sources/LoopAlgorithmRunner/main.swift b/Sources/LoopAlgorithmRunner/main.swift index 7eff454..03db067 100644 --- a/Sources/LoopAlgorithmRunner/main.swift +++ b/Sources/LoopAlgorithmRunner/main.swift @@ -28,7 +28,7 @@ func main() { do { let input = try readInputFile(inputFilePath) - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] diff --git a/Tests/LoopAlgorithmTests/AlgorithmEmulationTests.swift b/Tests/LoopAlgorithmTests/AlgorithmEmulationTests.swift index d77fdfc..3d0df0f 100644 --- a/Tests/LoopAlgorithmTests/AlgorithmEmulationTests.swift +++ b/Tests/LoopAlgorithmTests/AlgorithmEmulationTests.swift @@ -187,8 +187,8 @@ final class AlgorithmEmulationTests: XCTestCase { // a recommendation, and their forecasts should differ. var plain = fixture plain.emulation = nil - let emulated = LoopAlgorithm.run(input: fixture) - let current = LoopAlgorithm.run(input: plain) + let emulated = Algorithm.run(input: fixture) + let current = Algorithm.run(input: plain) XCTAssertNotNil(try? emulated.recommendationResult.get()) XCTAssertNotNil(try? current.recommendationResult.get()) } diff --git a/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift b/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift index b5bc2bc..b2b084e 100644 --- a/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift +++ b/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift @@ -46,10 +46,10 @@ final class AlgorithmExperimentsTests: XCTestCase { var input = AlgorithmInputFixture.mock(for: now) input.recommendationType = .automaticBolus input.carbEntries = [FixtureCarbEntry(absorptionTime: .hours(3), startDate: now.addingTimeInterval(.minutes(-30)), quantity: .carbs(30))] - let standard = LoopAlgorithm.run(input: input) + let standard = Algorithm.run(input: input) input.experiments = [.guardAndRunway] input.volatilityBaseline = 5 - let gated = LoopAlgorithm.run(input: input) + let gated = Algorithm.run(input: input) assertSameOutput(standard, gated) XCTAssertNil(standard.guardAndRunway) let output = gated.guardAndRunway! @@ -79,7 +79,7 @@ final class AlgorithmExperimentsTests: XCTestCase { let dense = flatForecast(200, from: now) let sparse = Array(dense.prefix(12)) + [dense.last!] func reserve(_ p: [PredictedGlucoseValue], _ volatility: Double) -> Double { - LoopAlgorithm.runwayAimShift(prediction: p, reserve: ExperimentParameters.RunwayReserve(), currentGlucose: 200, + Algorithm.runwayAimShift(prediction: p, reserve: ExperimentParameters.RunwayReserve(), currentGlucose: 200, activeCarbs: 0, volatility: volatility, sensitivity: 50, basalRate: 1.0, targetMidpoint: 105, suspendThreshold: 70, insulinModel: ExponentialInsulinModelPreset.rapidActingAdult) @@ -137,9 +137,9 @@ final class AlgorithmExperimentsTests: XCTestCase { let now = ISO8601DateFormatter().date(from: "2024-01-03T12:00:00+0000")! var input = AlgorithmInputFixture.mock(for: now) input.glucoseHistory = glucoseSeries([62, 66, 75, 90, 110, 130], endingAt: now.addingTimeInterval(.minutes(-1))) - let standard = LoopAlgorithm.run(input: input) + let standard = Algorithm.run(input: input) input.experiments = [.guardAndRunway] - let cut = LoopAlgorithm.run(input: input) + let cut = Algorithm.run(input: input) XCTAssertLessThan(cut.predictedGlucose.last!.quantity.doubleValue(for: unit), standard.predictedGlucose.last!.quantity.doubleValue(for: unit)) XCTAssertTrue(cut.guardAndRunway!.postLowRiseCutActive) @@ -180,7 +180,7 @@ final class AlgorithmExperimentsTests: XCTestCase { let now = Date() let glucose = (0...48).map { PredictedGlucoseValue(startDate: now.addingTimeInterval(.minutes(5 * Double($0))), quantity: .glucose(120)) } let band = ExperimentParameters.VolatilityBand(gain: 1.0) - let banded = LoopAlgorithm.volatilityBanded(glucose, band: band, volatility: 4.0) + let banded = Algorithm.volatilityBanded(glucose, band: band, volatility: 4.0) let offset: (Int) -> Double = { banded[$0].quantity.doubleValue(for: self.unit) - 120 } XCTAssertEqual(offset(0), 0, accuracy: 1e-9) XCTAssertEqual(offset(12), -4.0 * pow(12.0, 0.71), accuracy: 1e-9) // 60 min: full depth @@ -197,7 +197,7 @@ final class AlgorithmExperimentsTests: XCTestCase { private func shift(_ forecast: Double, current: Double? = nil, carbs: Double = 0, volatility: Double) -> Double { let now = Date() - return LoopAlgorithm.runwayAimShift( + return Algorithm.runwayAimShift( prediction: flatForecast(forecast, from: now), reserve: ExperimentParameters.RunwayReserve(), currentGlucose: current ?? forecast, activeCarbs: carbs, volatility: volatility, sensitivity: 50, basalRate: 1.0, targetMidpoint: 105, suspendThreshold: 70, @@ -211,7 +211,7 @@ final class AlgorithmExperimentsTests: XCTestCase { XCTAssertGreaterThan(shift(200, volatility: 2), 0) let now = Date() let short = Array(flatForecast(200, from: now).prefix(30)) // 145 min, short of 180 - XCTAssertEqual(LoopAlgorithm.runwayAimShift( + XCTAssertEqual(Algorithm.runwayAimShift( prediction: short, reserve: ExperimentParameters.RunwayReserve(), currentGlucose: 200, activeCarbs: 0, volatility: 2, sensitivity: 50, basalRate: 1.0, targetMidpoint: 105, suspendThreshold: 70, insulinModel: ExponentialInsulinModelPreset.rapidActingAdult), 0, "forecast shorter than the horizon") @@ -235,9 +235,9 @@ final class AlgorithmExperimentsTests: XCTestCase { for type in [DoseRecommendationType.tempBasal, .automaticBolus] { input.recommendationType = type input.experiments = [] - let standard = LoopAlgorithm.run(input: input) + let standard = Algorithm.run(input: input) input.experiments = [.guardAndRunway] - let runway = LoopAlgorithm.run(input: input) + let runway = Algorithm.run(input: input) XCTAssertEqual(runway.predictedGlucose, standard.predictedGlucose, "the forecast shown is not banded") let output = runway.guardAndRunway! let banded = output.guarded(runway.predictedGlucose).map { $0.quantity.doubleValue(for: unit) } @@ -260,9 +260,9 @@ final class AlgorithmExperimentsTests: XCTestCase { // The runway does not add to the manual recommendation; the band can only lower it. input.recommendationType = .manualBolus input.experiments = [] - let standard = LoopAlgorithm.run(input: input) + let standard = Algorithm.run(input: input) input.experiments = [.guardAndRunway] - let manual = LoopAlgorithm.run(input: input) + let manual = Algorithm.run(input: input) XCTAssertLessThanOrEqual(manual.recommendation?.manual?.amount ?? 0, standard.recommendation?.manual?.amount ?? 0) } @@ -280,14 +280,14 @@ final class AlgorithmExperimentsTests: XCTestCase { } let point = InsulinCorrection.aboveRange(min: g(120), correcting: g(200), minTarget: .glucose(100), units: 2) // A banded point below the suspend threshold suspends. - XCTAssertEqual(d(LoopAlgorithm.guarded(point, by: .suspend(min: g(65)))), "suspend 65") + XCTAssertEqual(d(Algorithm.guarded(point, by: .suspend(min: g(65)))), "suspend 65") // Otherwise the banded minimum is the one the guard checks; the units stay the forecast's. - XCTAssertEqual(d(LoopAlgorithm.guarded(point, by: .aboveRange(min: g(90), correcting: g(170), minTarget: .glucose(100), units: 1))), + XCTAssertEqual(d(Algorithm.guarded(point, by: .aboveRange(min: g(90), correcting: g(170), minTarget: .glucose(100), units: 1))), "above 90 200 2.0") - XCTAssertEqual(d(LoopAlgorithm.guarded(point, by: .entirelyBelowRange(min: g(85), minTarget: .glucose(100), units: -1))), + XCTAssertEqual(d(Algorithm.guarded(point, by: .entirelyBelowRange(min: g(85), minTarget: .glucose(100), units: -1))), "above 85 200 2.0") // A forecast that needs no correction is left as it is. - XCTAssertEqual(d(LoopAlgorithm.guarded(.inRange, by: .entirelyBelowRange(min: g(85), minTarget: .glucose(100), units: -1))), "in range") + XCTAssertEqual(d(Algorithm.guarded(.inRange, by: .entirelyBelowRange(min: g(85), minTarget: .glucose(100), units: -1))), "in range") } // MARK: - Slow retrospective correction @@ -304,7 +304,7 @@ final class AlgorithmExperimentsTests: XCTestCase { func testSlowRCLevelIsTheWindowedMeanResidual() { let now = Date(), p = ExperimentParameters.SlowRetrospectiveCorrection() // A constant 2 mg/dL per 5 min for 100 h, no carbs: only the last 72 h count, from zero at their start. - let level = LoopAlgorithm.slowRetrospectiveCorrectionLevel( + let level = Algorithm.slowRetrospectiveCorrectionLevel( counteraction: counteraction(2, hours: 100, endingAt: now), carbEffects: [], now: now, parameters: p) XCTAssertEqual(level, 2 * (1 - exp(-3.0)), accuracy: 0.01) // Carb effect is subtracted: a carb curve rising 2 mg/dL per 5 min explains all of it. @@ -313,17 +313,17 @@ final class AlgorithmExperimentsTests: XCTestCase { let date = now.addingTimeInterval(-Double(steps - i) * 300) return GlucoseEffect(startDate: date, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 2 * Double(i))) } - let explained = LoopAlgorithm.slowRetrospectiveCorrectionLevel( + let explained = Algorithm.slowRetrospectiveCorrectionLevel( counteraction: counteraction(2, hours: 100, endingAt: now), carbEffects: carbs, now: now, parameters: p) XCTAssertEqual(explained, 0, accuracy: 1e-9) } func testSlowRCMultiplierClamps() { let p = ExperimentParameters.SlowRetrospectiveCorrection() - XCTAssertEqual(LoopAlgorithm.slowRetrospectiveCorrectionMultiplier(level: 0, parameters: p), 1) - XCTAssertEqual(LoopAlgorithm.slowRetrospectiveCorrectionMultiplier(level: 2, parameters: p), 0.9, accuracy: 1e-12) - XCTAssertEqual(LoopAlgorithm.slowRetrospectiveCorrectionMultiplier(level: 20, parameters: p), 0.7) - XCTAssertEqual(LoopAlgorithm.slowRetrospectiveCorrectionMultiplier(level: -20, parameters: p), 1.3) + XCTAssertEqual(Algorithm.slowRetrospectiveCorrectionMultiplier(level: 0, parameters: p), 1) + XCTAssertEqual(Algorithm.slowRetrospectiveCorrectionMultiplier(level: 2, parameters: p), 0.9, accuracy: 1e-12) + XCTAssertEqual(Algorithm.slowRetrospectiveCorrectionMultiplier(level: 20, parameters: p), 0.7) + XCTAssertEqual(Algorithm.slowRetrospectiveCorrectionMultiplier(level: -20, parameters: p), 1.3) } func testSlowRCDosesMoreWhenGlucoseKeepsRisingUnexplained() { @@ -335,12 +335,12 @@ final class AlgorithmExperimentsTests: XCTestCase { input.glucoseHistory = glucoseSeries(values, endingAt: now.addingTimeInterval(.minutes(-1))) input.doses = [] input.carbEntries = [] - input.sensitivity = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: .glucose(50))] + input.sensitivity = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 50)] input.basal = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 1.0)] input.carbRatio = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 10)] - let standard = LoopAlgorithm.run(input: input) + let standard = Algorithm.run(input: input) input.experiments = [.slowRetrospectiveCorrection] - let slow = LoopAlgorithm.run(input: input) + let slow = Algorithm.run(input: input) XCTAssertNil(standard.slowRetrospectiveCorrection) let output = slow.slowRetrospectiveCorrection! XCTAssertGreaterThan(output.level, 0) @@ -350,12 +350,12 @@ final class AlgorithmExperimentsTests: XCTestCase { func testTargetLoweredFloors() { let now = Date() - let target: GlucoseRangeTimeline = [AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(.hours(1)), + let target: QuantityRangeTimeline = [AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(.hours(1)), value: ClosedRange(uncheckedBounds: (lower: .glucose(100), upper: .glucose(110))))] - let lowered = LoopAlgorithm.targetLowered(target, by: 25).first!.value + let lowered = Algorithm.targetLowered(target, by: 25).first!.value XCTAssertEqual(lowered.lowerBound.doubleValue(for: unit), 75) XCTAssertEqual(lowered.upperBound.doubleValue(for: unit), 85) - let floored = LoopAlgorithm.targetLowered(target, by: 60).first!.value + let floored = Algorithm.targetLowered(target, by: 60).first!.value XCTAssertEqual(floored.lowerBound.doubleValue(for: unit), 70) XCTAssertEqual(floored.upperBound.doubleValue(for: unit), 80) } @@ -394,9 +394,9 @@ final class AlgorithmExperimentsTests: XCTestCase { flag.useIntegralRetrospectiveCorrection = true var experiment = loadScenario(name) experiment.experiments = [.integralRetrospectiveCorrection] - assertSameOutput(LoopAlgorithm.run(input: flag), LoopAlgorithm.run(input: experiment)) + assertSameOutput(Algorithm.run(input: flag), Algorithm.run(input: experiment)) flag.experiments = [.integralRetrospectiveCorrection] // both: still integral RC, once - assertSameOutput(LoopAlgorithm.run(input: flag), LoopAlgorithm.run(input: experiment)) + assertSameOutput(Algorithm.run(input: flag), Algorithm.run(input: experiment)) } } @@ -423,16 +423,16 @@ final class AlgorithmExperimentsTests: XCTestCase { constant.automaticBolusApplicationFactor = 0.5 input.automaticBolusApplicationFactor = 0.4 input.experiments = [.glucoseBasedApplicationFactor] - let gbaf = LoopAlgorithm.run(input: input) - assertSameOutput(gbaf, LoopAlgorithm.run(input: constant)) + let gbaf = Algorithm.run(input: input) + assertSameOutput(gbaf, Algorithm.run(input: constant)) input.experiments = [] - XCTAssertGreaterThan(gbaf.recommendation!.automatic!.bolusUnits!, LoopAlgorithm.run(input: input).recommendation!.automatic!.bolusUnits!) + XCTAssertGreaterThan(gbaf.recommendation!.automatic!.bolusUnits!, Algorithm.run(input: input).recommendation!.automatic!.bolusUnits!) for type in [DoseRecommendationType.tempBasal, .manualBolus] { input.recommendationType = type input.experiments = [] - let standard = LoopAlgorithm.run(input: input) + let standard = Algorithm.run(input: input) input.experiments = [.glucoseBasedApplicationFactor] - assertSameOutput(standard, LoopAlgorithm.run(input: input)) + assertSameOutput(standard, Algorithm.run(input: input)) } } } diff --git a/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift b/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift index a1f8c3a..ace7cd4 100644 --- a/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift +++ b/Tests/LoopAlgorithmTests/CorrectionDosingTests.swift @@ -18,7 +18,7 @@ class CorrectionDosingTests: XCTestCase { let suspendThreshold: LoopQuantity = .glucose(55) let maxBasalRate = 3.0 - var target: GlucoseRangeTimeline! + var target: QuantityRangeTimeline! var sensitivity: [AbsoluteScheduleValue]! let basalRate = 1.0 let insulinModel = ExponentialInsulinModelPreset.rapidActingAdult @@ -43,7 +43,7 @@ class CorrectionDosingTests: XCTestCase { func testNoChange() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.noChangePrediction(), at: testDate, target: target, @@ -52,7 +52,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -64,7 +64,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, basalRate) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.5, neutralBasalRate: basalRate, @@ -77,7 +77,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.bolusUnits, 0) XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -89,7 +89,7 @@ class CorrectionDosingTests: XCTestCase { } func testStartHighEndInRange() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.startHighEndInRangePrediction(), at: testDate, target: target, @@ -98,7 +98,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -110,7 +110,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, basalRate) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.5, neutralBasalRate: basalRate, @@ -123,7 +123,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.bolusUnits, 0) XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -135,7 +135,7 @@ class CorrectionDosingTests: XCTestCase { } func testStartLowEndInRange() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.startLowEndInRangePrediction(), at: testDate, target: target, @@ -144,7 +144,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -156,7 +156,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 1) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -170,7 +170,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -182,7 +182,7 @@ class CorrectionDosingTests: XCTestCase { } func testCorrectLowAtMin() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.correctLowAtMin(), at: testDate, target: target, @@ -191,7 +191,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -203,7 +203,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 1) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -217,7 +217,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -230,7 +230,7 @@ class CorrectionDosingTests: XCTestCase { func testStartHighEndLow() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.startHighEndLow(), at: testDate, target: target, @@ -239,7 +239,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -251,7 +251,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 0) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -265,7 +265,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, 0) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -281,7 +281,7 @@ class CorrectionDosingTests: XCTestCase { } func testStartLowEndHigh() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.startLowEndHigh(), at: testDate, target: target, @@ -290,7 +290,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -302,7 +302,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 1.0) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -316,7 +316,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -332,7 +332,7 @@ class CorrectionDosingTests: XCTestCase { } func testFlatAndHigh() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.flatAndHigh(), at: testDate, target: target, @@ -341,7 +341,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -353,7 +353,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 3.0) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -367,7 +367,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -380,7 +380,7 @@ class CorrectionDosingTests: XCTestCase { func testHighAndFalling() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.highAndFalling(), at: testDate, target: target, @@ -389,7 +389,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -401,7 +401,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 1.63, accuracy: 0.05) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -415,7 +415,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -427,7 +427,7 @@ class CorrectionDosingTests: XCTestCase { } func testInRangeAndRising() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.inRangeAndRising(), at: testDate, target: target, @@ -436,7 +436,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -448,7 +448,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 1.63, accuracy: 0.05) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -462,7 +462,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -474,7 +474,7 @@ class CorrectionDosingTests: XCTestCase { } func testHighAndRising() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.highAndRising(), at: testDate, target: target, @@ -483,7 +483,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -495,7 +495,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 3.0, accuracy: 0.05) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -509,7 +509,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, basalRate) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -521,7 +521,7 @@ class CorrectionDosingTests: XCTestCase { } func testVeryLowAndRising() { - let correction = LoopAlgorithm.insulinCorrection( + let correction = Algorithm.insulinCorrection( prediction: PredictedGlucoseMocks.veryLowAndRising(), at: testDate, target: target, @@ -530,7 +530,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let recommendation = LoopAlgorithm.recommendTempBasal( + let recommendation = Algorithm.recommendTempBasal( for: correction, neutralBasalRate: basalRate, activeInsulin: 0, @@ -542,7 +542,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(recommendation.unitsPerHour, 0, accuracy: 0.05) XCTAssertEqual(recommendation.duration, .minutes(30)) - let automaticDose = LoopAlgorithm.recommendAutomaticDose( + let automaticDose = Algorithm.recommendAutomaticDose( for: correction, applicationFactor: 0.4, neutralBasalRate: basalRate, @@ -556,7 +556,7 @@ class CorrectionDosingTests: XCTestCase { XCTAssertEqual(automaticDose.basalAdjustment.unitsPerHour, 0) XCTAssertEqual(automaticDose.basalAdjustment.duration, .minutes(30)) - let manualDose = LoopAlgorithm.recommendManualBolus( + let manualDose = Algorithm.recommendManualBolus( for: correction, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: PredictedGlucoseMocks.testDate, quantity: .glucose(120)), @@ -580,7 +580,7 @@ class CorrectionDosingTests: XCTestCase { // Target switches from 90-120 to 150-170 at t+2h; predicted points beyond // the boundary are already in range, so they should require no correction. - let steppedTarget: GlucoseRangeTimeline = [ + let steppedTarget: QuantityRangeTimeline = [ AbsoluteScheduleValue( startDate: testDate.addingTimeInterval(.hours(-24)), endDate: testDate.addingTimeInterval(.hours(2)), @@ -593,7 +593,7 @@ class CorrectionDosingTests: XCTestCase { ) ] - let uniformCorrection = LoopAlgorithm.insulinCorrection( + let uniformCorrection = Algorithm.insulinCorrection( prediction: prediction, at: testDate, target: target, @@ -602,7 +602,7 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let steppedCorrection = LoopAlgorithm.insulinCorrection( + let steppedCorrection = Algorithm.insulinCorrection( prediction: prediction, at: testDate, target: steppedTarget, @@ -611,14 +611,14 @@ class CorrectionDosingTests: XCTestCase { insulinModel: insulinModel ) - let uniformDose = LoopAlgorithm.recommendManualBolus( + let uniformDose = Algorithm.recommendManualBolus( for: uniformCorrection, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: testDate, quantity: .glucose(160)), target: target ) - let steppedDose = LoopAlgorithm.recommendManualBolus( + let steppedDose = Algorithm.recommendManualBolus( for: steppedCorrection, maxBolus: 6, currentGlucose: FixtureGlucoseSample(startDate: testDate, quantity: .glucose(160)), diff --git a/Tests/LoopAlgorithmTests/DateRoundingTests.swift b/Tests/LoopAlgorithmTests/DateRoundingTests.swift new file mode 100644 index 0000000..cfee3cf --- /dev/null +++ b/Tests/LoopAlgorithmTests/DateRoundingTests.swift @@ -0,0 +1,66 @@ +// +// DateRoundingTests.swift +// LoopAlgorithm +// +// Created by Nathan Racklyeft on 1/18/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import XCTest +@testable import LoopAlgorithm + +class DateRoundingTests: XCTestCase { + + func testDateCeiledToInterval() { + let calendar = Calendar.current + + let five01 = calendar.nextDate(after: Date(), matching: DateComponents(hour: 5, minute: 0, second: 1), matchingPolicy: .nextTime)! + + let five05 = calendar.nextDate(after: five01, matching: DateComponents(hour: 5, minute: 5, second: 0), matchingPolicy: .nextTime)! + + XCTAssertEqual(five05, five01.dateCeiledToTimeInterval(TimeInterval(minutes: 5))) + + let six = calendar.nextDate(after: five01, matching: DateComponents(hour: 6, minute: 0, second: 0), matchingPolicy: .nextTime)! + + XCTAssertEqual(six, five01.dateCeiledToTimeInterval(TimeInterval(minutes: 60))) + + XCTAssertEqual(five05, five05.dateCeiledToTimeInterval(TimeInterval(minutes: 5))) + + let five47 = calendar.nextDate(after: five01, matching: DateComponents(hour: 5, minute: 47, second: 58), matchingPolicy: .nextTime)! + + let five50 = calendar.nextDate(after: five01, matching: DateComponents(hour: 5, minute: 50, second: 0), matchingPolicy: .nextTime)! + + XCTAssertEqual(five50, five47.dateCeiledToTimeInterval(TimeInterval(minutes: 5))) + + let twentyThree59 = calendar.nextDate(after: five01, matching: DateComponents(hour: 23, minute: 59, second: 0), matchingPolicy: .nextTime)! + + let tomorrowMidnight = calendar.nextDate(after: five01, matching: DateComponents(hour: 0, minute: 0, second: 0), matchingPolicy: .nextTime)! + + XCTAssertEqual(tomorrowMidnight, twentyThree59.dateCeiledToTimeInterval(TimeInterval(minutes: 5))) + + XCTAssertEqual(five01, five01.dateCeiledToTimeInterval(TimeInterval(0))) + } + + func testDateFlooredToInterval() { + let calendar = Calendar.current + + let five01 = calendar.nextDate(after: Date(), matching: DateComponents(hour: 5, minute: 0, second: 1), matchingPolicy: .nextTime)! + + let five = calendar.nextDate(after: five01, matching: DateComponents(hour: 5, minute: 0, second: 0), matchingPolicy: .nextTime, direction: .backward)! + + XCTAssertEqual(five, five01.dateFlooredToTimeInterval(TimeInterval(minutes: 5))) + + let five59 = calendar.nextDate(after: five01, matching: DateComponents(hour: 5, minute: 59, second: 0), matchingPolicy: .nextTime)! + + XCTAssertEqual(five, five59.dateFlooredToTimeInterval(TimeInterval(minutes: 60))) + + let five55 = calendar.nextDate(after: five01, matching: DateComponents(hour: 5, minute: 55, second: 0), matchingPolicy: .nextTime)! + + XCTAssertEqual(five55, five59.dateFlooredToTimeInterval(TimeInterval(minutes: 5))) + + + XCTAssertEqual(five, five.dateFlooredToTimeInterval(TimeInterval(minutes: 5))) + + XCTAssertEqual(five01, five01.dateFlooredToTimeInterval(TimeInterval(0))) + } +} diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_carb_effect_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_carb_effect_input.json new file mode 100644 index 0000000..c72bb99 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_carb_effect_input.json @@ -0,0 +1,467 @@ +[ + { + "date": "2015-10-16T05:55:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:00:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:05:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:10:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:15:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:20:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:25:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:30:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:35:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:40:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:45:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:50:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:55:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:00:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:05:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:10:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:15:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:20:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:25:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:30:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:35:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:40:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:45:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:50:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:55:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:00:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:05:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:10:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:15:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:20:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:25:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:30:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:35:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:40:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:45:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:50:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:55:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:00:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:05:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:10:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:15:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:20:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:25:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:30:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:35:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:40:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:45:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:50:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:55:00", + "amount": 0.08920096021947878, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:00:00", + "amount": 0.6130692729766802, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:05:00", + "amount": 1.6060733882030176, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:10:00", + "amount": 3.0682133058984906, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:15:00", + "amount": 4.999489026063102, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:20:00", + "amount": 7.399900548696846, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:25:00", + "amount": 10.269447873799727, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:30:00", + "amount": 13.608131001371746, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:35:00", + "amount": 17.415949931412896, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:40:00", + "amount": 21.692904663923187, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:45:00", + "amount": 26.438995198902607, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:50:00", + "amount": 31.65422153635116, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:55:00", + "amount": 37.33858367626886, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:00:00", + "amount": 43.49208161865569, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:05:00", + "amount": 50.11471536351166, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:10:00", + "amount": 57.20648491083676, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:15:00", + "amount": 64.767390260631, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:20:00", + "amount": 72.79743141289437, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:25:00", + "amount": 81.11820644718794, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:30:00", + "amount": 89.03878257887516, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:35:00", + "amount": 96.49022290809329, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:40:00", + "amount": 103.47252743484226, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:45:00", + "amount": 109.98569615912207, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:50:00", + "amount": 116.02972908093281, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:55:00", + "amount": 121.60462620027438, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:00:00", + "amount": 126.71038751714674, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:05:00", + "amount": 131.34701303155006, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:10:00", + "amount": 135.51450274348426, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:15:00", + "amount": 139.21285665294928, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:20:00", + "amount": 142.44207475994514, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:25:00", + "amount": 145.20215706447186, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:30:00", + "amount": 147.4931035665295, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:35:00", + "amount": 149.31491426611802, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:40:00", + "amount": 150.66758916323732, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:45:00", + "amount": 151.5511282578875, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:50:00", + "amount": 151.9655315500686, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:55:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:00:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:05:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:10:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:15:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:20:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:25:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:30:00", + "amount": 152.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:35:00", + "amount": 152.0, + "unit": "mg/dL" + } +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_glucose_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_glucose_input.json new file mode 100644 index 0000000..bc74b56 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_glucose_input.json @@ -0,0 +1,6 @@ +[{ + "trend_arrow": "FLAT", + "system_time": "2015-10-16T16:51:46", + "display_time": "2015-10-16T09:51:08", + "glucose": 147 +}] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_insulin_effect_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_insulin_effect_input.json new file mode 100644 index 0000000..244bb03 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_insulin_effect_input.json @@ -0,0 +1,527 @@ +[ + { + "date": "2015-10-16T05:55:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:00:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:05:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:10:00", + "amount": -0.05116973249986715, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:15:00", + "amount": -0.05789217482631093, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:20:00", + "amount": -0.07771134754398137, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:25:00", + "amount": -0.09750451603513274, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:30:00", + "amount": -0.12203773711068157, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:35:00", + "amount": -0.15095055115286835, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:40:00", + "amount": -0.1838927275472713, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:45:00", + "amount": -0.22052426468279615, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:50:00", + "amount": -0.24717183578216545, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T06:55:00", + "amount": -0.2893144242175132, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:00:00", + "amount": -0.2589871828832556, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:05:00", + "amount": -0.29857264308450326, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:10:00", + "amount": -0.22330600448031762, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:15:00", + "amount": -0.24340112507542233, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:20:00", + "amount": -0.25058050652147246, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:25:00", + "amount": -0.24254143594819094, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:30:00", + "amount": -0.21749871628061584, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:35:00", + "amount": -0.1772099748720401, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:40:00", + "amount": -0.12373296368553532, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:45:00", + "amount": -0.05800053902807922, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:50:00", + "amount": 0.019065916806689343, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T07:55:00", + "amount": 0.10656982887179145, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:00:00", + "amount": 0.5814672957555429, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:05:00", + "amount": 0.6964983023063709, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:10:00", + "amount": 0.839841951080192, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:15:00", + "amount": 1.017714433545975, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:20:00", + "amount": 1.2358473523026858, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:25:00", + "amount": 1.4990738388257407, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:30:00", + "amount": 2.4424187692212054, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:35:00", + "amount": 2.803912423996154, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:40:00", + "amount": 3.2220002596052173, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:45:00", + "amount": 3.700352667321728, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:50:00", + "amount": 4.242235634301219, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T08:55:00", + "amount": 4.850524811081413, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:00:00", + "amount": 5.5277195790822375, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:05:00", + "amount": 6.275957118105808, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:10:00", + "amount": 7.092719849613544, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:15:00", + "amount": 7.966122324754094, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:20:00", + "amount": 8.889869513880269, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:25:00", + "amount": 9.55106827366706, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:30:00", + "amount": 10.536493085687397, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:35:00", + "amount": 10.93556951143635, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:40:00", + "amount": 11.890699805221772, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:45:00", + "amount": 12.81049769931439, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:50:00", + "amount": 13.681466188886816, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:55:00", + "amount": 13.910708100083081, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:00:00", + "amount": 13.508961468585511, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:05:00", + "amount": 12.466684146484779, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:10:00", + "amount": 10.781837253440187, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:15:00", + "amount": 9.0846795883076, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:20:00", + "amount": 7.013205180933435, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:25:00", + "amount": 4.568371172724007, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:30:00", + "amount": 1.767024244038235, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:35:00", + "amount": -1.3622394338251143, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:40:00", + "amount": -4.785438162555244, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:45:00", + "amount": -8.475077970817363, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:50:00", + "amount": -12.404320524954366, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:55:00", + "amount": -16.54706904749455, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:00:00", + "amount": -20.880282108817163, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:05:00", + "amount": -25.378865830960304, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:10:00", + "amount": -30.02894037283888, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:15:00", + "amount": -34.80866756164933, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:20:00", + "amount": -39.69232924579666, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:25:00", + "amount": -44.65904560451495, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:30:00", + "amount": -49.687928735135834, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:35:00", + "amount": -54.75929582600946, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:40:00", + "amount": -59.84482763266896, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:45:00", + "amount": -64.92715822050762, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:50:00", + "amount": -69.98976050270197, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T11:55:00", + "amount": -75.01694624021185, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:00:00", + "amount": -79.9932139266723, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:05:00", + "amount": -84.89974475270296, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:10:00", + "amount": -89.7546057953877, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:15:00", + "amount": -94.54389486544915, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:20:00", + "amount": -99.25471446517622, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:25:00", + "amount": -103.87808111694163, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:30:00", + "amount": -108.39054448779835, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:35:00", + "amount": -112.79562633788349, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:40:00", + "amount": -117.0769986742359, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:45:00", + "amount": -121.23136988437481, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:50:00", + "amount": -125.25210996088262, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T12:55:00", + "amount": -129.13368784015748, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:00:00", + "amount": -132.87168546991376, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:05:00", + "amount": -136.46281187668208, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:10:00", + "amount": -139.91076109601062, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:15:00", + "amount": -143.1920024921548, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:20:00", + "amount": -146.2986738071368, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:25:00", + "amount": -149.23710482648195, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:30:00", + "amount": -152.01257516166288, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:35:00", + "amount": -154.5675135333564, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:40:00", + "amount": -156.9226367135027, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:45:00", + "amount": -159.09407074832978, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:50:00", + "amount": -161.0914402564, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T13:55:00", + "amount": -163.0926823408534, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:00:00", + "amount": -163.39678416635502, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:05:00", + "amount": -163.65380824473786, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:10:00", + "amount": -163.86314957675157, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:15:00", + "amount": -164.02799500404936, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:20:00", + "amount": -164.15136192522291, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:25:00", + "amount": -164.2348489355287, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:30:00", + "amount": -164.28097710009604, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T14:35:00", + "amount": -164.2833333333333, + "unit": "mg/dL" + } +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_glucose_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_glucose_input.json new file mode 100644 index 0000000..4ee6599 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_glucose_input.json @@ -0,0 +1,20 @@ +[ + { + "trend_arrow": "FLAT", + "system_time": "2015-10-30T17:16:36", + "display_time": "2015-10-30T09:17:27", + "glucose": 111 + }, + { + "trend_arrow": "FLAT", + "system_time": "2015-10-30T17:11:36", + "display_time": "2015-10-30T09:12:27", + "glucose": 111 + }, + { + "trend_arrow": "FLAT", + "system_time": "2015-10-30T17:06:36", + "display_time": "2015-10-30T09:07:28", + "glucose": 113 + } +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_insulin_effect_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_insulin_effect_input.json new file mode 100644 index 0000000..4d2a4a5 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_insulin_effect_input.json @@ -0,0 +1,37 @@ +[ + { + "date": "2015-10-30T09:15:00", + "amount": -96.64432176805616, + "unit": "mg/dL" + }, + { + "date": "2015-10-30T09:20:00", + "amount": -96.91899878577772, + "unit": "mg/dL" + }, + { + "date": "2015-10-30T09:25:00", + "amount": -97.14706680030386, + "unit": "mg/dL" + }, + { + "date": "2015-10-30T09:30:00", + "amount": -97.30954106615258, + "unit": "mg/dL" + }, + { + "date": "2015-10-30T09:35:00", + "amount": -97.40780654695538, + "unit": "mg/dL" + }, + { + "date": "2015-10-30T09:40:00", + "amount": -97.44702440741187, + "unit": "mg/dL" + }, + { + "date": "2015-10-30T09:45:00", + "amount": -97.428432128785, + "unit": "mg/dL" + } +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_momentum_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_momentum_input.json new file mode 100644 index 0000000..e58c381 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_momentum_input.json @@ -0,0 +1,7 @@ +[ + {"amount": -0.00, "date": "2015-10-30T09:15:00", "unit": "mg/dL"}, + {"amount": -0.51, "date": "2015-10-30T09:20:00", "unit": "mg/dL"}, + {"amount": -1.51, "date": "2015-10-30T09:25:00", "unit": "mg/dL"}, + {"amount": -2.51, "date": "2015-10-30T09:30:00", "unit": "mg/dL"}, + {"amount": -3.51, "date": "2015-10-30T09:35:00", "unit": "mg/dL"} +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_output.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_output.json new file mode 100644 index 0000000..a28bd15 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_blend_output.json @@ -0,0 +1 @@ +[{"date": "2015-10-30T09:17:27", "amount": 111.0, "unit": "mg/dL"}, {"date": "2015-10-30T09:20:00", "amount": 110.49, "unit": "mg/dL"}, {"date": "2015-10-30T09:25:00", "amount": 109.62122843753055, "unit": "mg/dL"}, {"date": "2015-10-30T09:30:00", "amount": 109.04278305705336, "unit": "mg/dL"}, {"date": "2015-10-30T09:35:00", "amount": 108.79723427144835, "unit": "mg/dL"}, {"date": "2015-10-30T09:40:00", "amount": 108.75801641099186, "unit": "mg/dL"}, {"date": "2015-10-30T09:45:00", "amount": 108.77660868961873, "unit": "mg/dL"}] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_input.json new file mode 100644 index 0000000..b7247a1 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_input.json @@ -0,0 +1,37 @@ +[ + { + "date": "2015-10-16T09:50:00", + "amount": -11.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:55:00", + "amount": -14.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:00:00", + "amount": -17.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:05:00", + "amount": -20.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:10:00", + "amount": -23.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:15:00", + "amount": -26.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:20:00", + "amount": -29.0, + "unit": "mg/dL" + } +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_output.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_output.json new file mode 100644 index 0000000..8da0604 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_down_output.json @@ -0,0 +1 @@ +[{"date": "2015-10-16T09:51:08", "amount": 147.0, "unit": "mg/dL"}, {"date": "2015-10-16T09:55:00", "amount": 144.0, "unit": "mg/dL"}, {"date": "2015-10-16T10:00:00", "amount": 141.48288815336815, "unit": "mg/dL"}, {"date": "2015-10-16T10:05:00", "amount": 139.52941258933004, "unit": "mg/dL"}, {"date": "2015-10-16T10:10:00", "amount": 138.06988445366972, "unit": "mg/dL"}, {"date": "2015-10-16T10:15:00", "amount": 137.51056554586722, "unit": "mg/dL"}, {"date": "2015-10-16T10:20:00", "amount": 137.68859084523504, "unit": "mg/dL"}, {"date": "2015-10-16T10:25:00", "amount": 138.1133041621285, "unit": "mg/dL"}, {"date": "2015-10-16T10:30:00", "amount": 138.65064036101472, "unit": "mg/dL"}, {"date": "2015-10-16T10:35:00", "amount": 139.32919561319252, "unit": "mg/dL"}, {"date": "2015-10-16T10:40:00", "amount": 140.18295161697267, "unit": "mg/dL"}, {"date": "2015-10-16T10:45:00", "amount": 141.23940234368996, "unit": "mg/dL"}, {"date": "2015-10-16T10:50:00", "amount": 142.5253861270015, "unit": "mg/dL"}, {"date": "2015-10-16T10:55:00", "amount": 144.06699974437902, "unit": "mg/dL"}, {"date": "2015-10-16T11:00:00", "amount": 145.88728462544324, "unit": "mg/dL"}, {"date": "2015-10-16T11:05:00", "amount": 148.01133464815607, "unit": "mg/dL"}, {"date": "2015-10-16T11:10:00", "amount": 150.45302965360258, "unit": "mg/dL"}, {"date": "2015-10-16T11:15:00", "amount": 153.23420781458637, "unit": "mg/dL"}, {"date": "2015-10-16T11:20:00", "amount": 156.3805872827024, "unit": "mg/dL"}, {"date": "2015-10-16T11:25:00", "amount": 159.7346459582777, "unit": "mg/dL"}, {"date": "2015-10-16T11:30:00", "amount": 162.62633895934403, "unit": "mg/dL"}, {"date": "2015-10-16T11:35:00", "amount": 165.00641219768852, "unit": "mg/dL"}, {"date": "2015-10-16T11:40:00", "amount": 166.903184917778, "unit": "mg/dL"}, {"date": "2015-10-16T11:45:00", "amount": 168.33402305421913, "unit": "mg/dL"}, {"date": "2015-10-16T11:50:00", "amount": 169.31545369383554, "unit": "mg/dL"}, {"date": "2015-10-16T11:55:00", "amount": 169.8631650756672, "unit": "mg/dL"}, {"date": "2015-10-16T12:00:00", "amount": 169.9926587060791, "unit": "mg/dL"}, {"date": "2015-10-16T12:05:00", "amount": 169.7227533944518, "unit": "mg/dL"}, {"date": "2015-10-16T12:10:00", "amount": 169.03538206370123, "unit": "mg/dL"}, {"date": "2015-10-16T12:15:00", "amount": 167.94444690310482, "unit": "mg/dL"}, {"date": "2015-10-16T12:20:00", "amount": 166.4628454103736, "unit": "mg/dL"}, {"date": "2015-10-16T12:25:00", "amount": 164.5995610631349, "unit": "mg/dL"}, {"date": "2015-10-16T12:30:00", "amount": 162.3780441943358, "unit": "mg/dL"}, {"date": "2015-10-16T12:35:00", "amount": 159.7947730438392, "unit": "mg/dL"}, {"date": "2015-10-16T12:40:00", "amount": 156.8660756046061, "unit": "mg/dL"}, {"date": "2015-10-16T12:45:00", "amount": 153.59524348911737, "unit": "mg/dL"}, {"date": "2015-10-16T12:50:00", "amount": 149.98890670479062, "unit": "mg/dL"}, {"date": "2015-10-16T12:55:00", "amount": 146.14179727544717, "unit": "mg/dL"}, {"date": "2015-10-16T13:00:00", "amount": 142.4037996456909, "unit": "mg/dL"}, {"date": "2015-10-16T13:05:00", "amount": 138.81267323892257, "unit": "mg/dL"}, {"date": "2015-10-16T13:10:00", "amount": 135.36472401959404, "unit": "mg/dL"}, {"date": "2015-10-16T13:15:00", "amount": 132.08348262344984, "unit": "mg/dL"}, {"date": "2015-10-16T13:20:00", "amount": 128.97681130846786, "unit": "mg/dL"}, {"date": "2015-10-16T13:25:00", "amount": 126.0383802891227, "unit": "mg/dL"}, {"date": "2015-10-16T13:30:00", "amount": 123.26290995394177, "unit": "mg/dL"}, {"date": "2015-10-16T13:35:00", "amount": 120.70797158224826, "unit": "mg/dL"}, {"date": "2015-10-16T13:40:00", "amount": 118.35284840210196, "unit": "mg/dL"}, {"date": "2015-10-16T13:45:00", "amount": 116.18141436727487, "unit": "mg/dL"}, {"date": "2015-10-16T13:50:00", "amount": 114.18404485920465, "unit": "mg/dL"}, {"date": "2015-10-16T13:55:00", "amount": 112.18280277475125, "unit": "mg/dL"}, {"date": "2015-10-16T14:00:00", "amount": 111.87870094924963, "unit": "mg/dL"}, {"date": "2015-10-16T14:05:00", "amount": 111.62167687086679, "unit": "mg/dL"}, {"date": "2015-10-16T14:10:00", "amount": 111.41233553885309, "unit": "mg/dL"}, {"date": "2015-10-16T14:15:00", "amount": 111.2474901115553, "unit": "mg/dL"}, {"date": "2015-10-16T14:20:00", "amount": 111.12412319038174, "unit": "mg/dL"}, {"date": "2015-10-16T14:25:00", "amount": 111.04063618007595, "unit": "mg/dL"}, {"date": "2015-10-16T14:30:00", "amount": 110.99450801550861, "unit": "mg/dL"}, {"date": "2015-10-16T14:35:00", "amount": 110.99215178227135, "unit": "mg/dL"}] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_glucose_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_glucose_input.json new file mode 100644 index 0000000..2ca9fb4 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_glucose_input.json @@ -0,0 +1,6 @@ +[{ + "trend_arrow": "FLAT", + "system_time": "2015-10-16T16:54:46", + "display_time": "2015-10-16T09:54:08", + "glucose": 147 +}] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_input.json new file mode 100644 index 0000000..3b4fbe0 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_input.json @@ -0,0 +1,37 @@ +[ + { + "date": "2015-10-16T09:50:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:55:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:00:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:05:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:10:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:15:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:20:00", + "amount": 0.0, + "unit": "mg/dL" + } +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_output.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_output.json new file mode 100644 index 0000000..d622b69 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_flat_output.json @@ -0,0 +1 @@ +[{"date": "2015-10-16T09:54:08", "amount": 147.0, "unit": "mg/dL"}, {"date": "2015-10-16T09:55:00", "amount": 147.0, "unit": "mg/dL"}, {"date": "2015-10-16T10:00:00", "amount": 147.004233551617, "unit": "mg/dL"}, {"date": "2015-10-16T10:05:00", "amount": 146.9926707724038, "unit": "mg/dL"}, {"date": "2015-10-16T10:10:00", "amount": 146.8958674737854, "unit": "mg/dL"}, {"date": "2015-10-16T10:15:00", "amount": 147.04445439937905, "unit": "mg/dL"}, {"date": "2015-10-16T10:20:00", "amount": 147.31900724491572, "unit": "mg/dL"}, {"date": "2015-10-16T10:25:00", "amount": 147.74372056180917, "unit": "mg/dL"}, {"date": "2015-10-16T10:30:00", "amount": 148.2810567606954, "unit": "mg/dL"}, {"date": "2015-10-16T10:35:00", "amount": 148.9596120128732, "unit": "mg/dL"}, {"date": "2015-10-16T10:40:00", "amount": 149.81336801665336, "unit": "mg/dL"}, {"date": "2015-10-16T10:45:00", "amount": 150.86981874337064, "unit": "mg/dL"}, {"date": "2015-10-16T10:50:00", "amount": 152.15580252668218, "unit": "mg/dL"}, {"date": "2015-10-16T10:55:00", "amount": 153.6974161440597, "unit": "mg/dL"}, {"date": "2015-10-16T11:00:00", "amount": 155.51770102512393, "unit": "mg/dL"}, {"date": "2015-10-16T11:05:00", "amount": 157.64175104783675, "unit": "mg/dL"}, {"date": "2015-10-16T11:10:00", "amount": 160.08344605328327, "unit": "mg/dL"}, {"date": "2015-10-16T11:15:00", "amount": 162.86462421426705, "unit": "mg/dL"}, {"date": "2015-10-16T11:20:00", "amount": 166.0110036823831, "unit": "mg/dL"}, {"date": "2015-10-16T11:25:00", "amount": 169.36506235795838, "unit": "mg/dL"}, {"date": "2015-10-16T11:30:00", "amount": 172.2567553590247, "unit": "mg/dL"}, {"date": "2015-10-16T11:35:00", "amount": 174.6368285973692, "unit": "mg/dL"}, {"date": "2015-10-16T11:40:00", "amount": 176.53360131745868, "unit": "mg/dL"}, {"date": "2015-10-16T11:45:00", "amount": 177.96443945389981, "unit": "mg/dL"}, {"date": "2015-10-16T11:50:00", "amount": 178.9458700935162, "unit": "mg/dL"}, {"date": "2015-10-16T11:55:00", "amount": 179.49358147534787, "unit": "mg/dL"}, {"date": "2015-10-16T12:00:00", "amount": 179.62307510575977, "unit": "mg/dL"}, {"date": "2015-10-16T12:05:00", "amount": 179.35316979413244, "unit": "mg/dL"}, {"date": "2015-10-16T12:10:00", "amount": 178.6657984633819, "unit": "mg/dL"}, {"date": "2015-10-16T12:15:00", "amount": 177.57486330278547, "unit": "mg/dL"}, {"date": "2015-10-16T12:20:00", "amount": 176.09326181005426, "unit": "mg/dL"}, {"date": "2015-10-16T12:25:00", "amount": 174.22997746281555, "unit": "mg/dL"}, {"date": "2015-10-16T12:30:00", "amount": 172.00846059401647, "unit": "mg/dL"}, {"date": "2015-10-16T12:35:00", "amount": 169.42518944351986, "unit": "mg/dL"}, {"date": "2015-10-16T12:40:00", "amount": 166.49649200428675, "unit": "mg/dL"}, {"date": "2015-10-16T12:45:00", "amount": 163.22565988879802, "unit": "mg/dL"}, {"date": "2015-10-16T12:50:00", "amount": 159.61932310447128, "unit": "mg/dL"}, {"date": "2015-10-16T12:55:00", "amount": 155.77221367512783, "unit": "mg/dL"}, {"date": "2015-10-16T13:00:00", "amount": 152.03421604537155, "unit": "mg/dL"}, {"date": "2015-10-16T13:05:00", "amount": 148.44308963860323, "unit": "mg/dL"}, {"date": "2015-10-16T13:10:00", "amount": 144.9951404192747, "unit": "mg/dL"}, {"date": "2015-10-16T13:15:00", "amount": 141.7138990231305, "unit": "mg/dL"}, {"date": "2015-10-16T13:20:00", "amount": 138.6072277081485, "unit": "mg/dL"}, {"date": "2015-10-16T13:25:00", "amount": 135.66879668880335, "unit": "mg/dL"}, {"date": "2015-10-16T13:30:00", "amount": 132.89332635362243, "unit": "mg/dL"}, {"date": "2015-10-16T13:35:00", "amount": 130.33838798192892, "unit": "mg/dL"}, {"date": "2015-10-16T13:40:00", "amount": 127.98326480178261, "unit": "mg/dL"}, {"date": "2015-10-16T13:45:00", "amount": 125.81183076695552, "unit": "mg/dL"}, {"date": "2015-10-16T13:50:00", "amount": 123.8144612588853, "unit": "mg/dL"}, {"date": "2015-10-16T13:55:00", "amount": 121.81321917443191, "unit": "mg/dL"}, {"date": "2015-10-16T14:00:00", "amount": 121.50911734893029, "unit": "mg/dL"}, {"date": "2015-10-16T14:05:00", "amount": 121.25209327054745, "unit": "mg/dL"}, {"date": "2015-10-16T14:10:00", "amount": 121.04275193853374, "unit": "mg/dL"}, {"date": "2015-10-16T14:15:00", "amount": 120.87790651123595, "unit": "mg/dL"}, {"date": "2015-10-16T14:20:00", "amount": 120.7545395900624, "unit": "mg/dL"}, {"date": "2015-10-16T14:25:00", "amount": 120.6710525797566, "unit": "mg/dL"}, {"date": "2015-10-16T14:30:00", "amount": 120.62492441518927, "unit": "mg/dL"}, {"date": "2015-10-16T14:35:00", "amount": 120.622568181952, "unit": "mg/dL"}] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_input.json new file mode 100644 index 0000000..8d069d1 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_input.json @@ -0,0 +1,37 @@ +[ + { + "date": "2015-10-16T09:50:00", + "amount": 0.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T09:55:00", + "amount": 3.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:00:00", + "amount": 6.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:05:00", + "amount": 9.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:10:00", + "amount": 12.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:15:00", + "amount": 15.0, + "unit": "mg/dL" + }, + { + "date": "2015-10-16T10:20:00", + "amount": 18.0, + "unit": "mg/dL" + } +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_output.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_output.json new file mode 100644 index 0000000..c7c957c --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_momentum_up_output.json @@ -0,0 +1 @@ +[{"date": "2015-10-16T09:51:08", "amount": 147.0, "unit": "mg/dL"}, {"date": "2015-10-16T09:55:00", "amount": 150.0, "unit": "mg/dL"}, {"date": "2015-10-16T10:00:00", "amount": 152.55488815336815, "unit": "mg/dL"}, {"date": "2015-10-16T10:05:00", "amount": 154.47341258933002, "unit": "mg/dL"}, {"date": "2015-10-16T10:10:00", "amount": 155.6858844536697, "unit": "mg/dL"}, {"date": "2015-10-16T10:15:00", "amount": 156.5985655458672, "unit": "mg/dL"}, {"date": "2015-10-16T10:20:00", "amount": 157.048590845235, "unit": "mg/dL"}, {"date": "2015-10-16T10:25:00", "amount": 157.47330416212844, "unit": "mg/dL"}, {"date": "2015-10-16T10:30:00", "amount": 158.01064036101468, "unit": "mg/dL"}, {"date": "2015-10-16T10:35:00", "amount": 158.68919561319248, "unit": "mg/dL"}, {"date": "2015-10-16T10:40:00", "amount": 159.54295161697263, "unit": "mg/dL"}, {"date": "2015-10-16T10:45:00", "amount": 160.59940234368992, "unit": "mg/dL"}, {"date": "2015-10-16T10:50:00", "amount": 161.88538612700145, "unit": "mg/dL"}, {"date": "2015-10-16T10:55:00", "amount": 163.42699974437897, "unit": "mg/dL"}, {"date": "2015-10-16T11:00:00", "amount": 165.2472846254432, "unit": "mg/dL"}, {"date": "2015-10-16T11:05:00", "amount": 167.37133464815602, "unit": "mg/dL"}, {"date": "2015-10-16T11:10:00", "amount": 169.81302965360254, "unit": "mg/dL"}, {"date": "2015-10-16T11:15:00", "amount": 172.59420781458633, "unit": "mg/dL"}, {"date": "2015-10-16T11:20:00", "amount": 175.74058728270236, "unit": "mg/dL"}, {"date": "2015-10-16T11:25:00", "amount": 179.09464595827765, "unit": "mg/dL"}, {"date": "2015-10-16T11:30:00", "amount": 181.98633895934398, "unit": "mg/dL"}, {"date": "2015-10-16T11:35:00", "amount": 184.36641219768848, "unit": "mg/dL"}, {"date": "2015-10-16T11:40:00", "amount": 186.26318491777795, "unit": "mg/dL"}, {"date": "2015-10-16T11:45:00", "amount": 187.6940230542191, "unit": "mg/dL"}, {"date": "2015-10-16T11:50:00", "amount": 188.6754536938355, "unit": "mg/dL"}, {"date": "2015-10-16T11:55:00", "amount": 189.22316507566717, "unit": "mg/dL"}, {"date": "2015-10-16T12:00:00", "amount": 189.35265870607907, "unit": "mg/dL"}, {"date": "2015-10-16T12:05:00", "amount": 189.08275339445174, "unit": "mg/dL"}, {"date": "2015-10-16T12:10:00", "amount": 188.3953820637012, "unit": "mg/dL"}, {"date": "2015-10-16T12:15:00", "amount": 187.30444690310478, "unit": "mg/dL"}, {"date": "2015-10-16T12:20:00", "amount": 185.82284541037356, "unit": "mg/dL"}, {"date": "2015-10-16T12:25:00", "amount": 183.95956106313486, "unit": "mg/dL"}, {"date": "2015-10-16T12:30:00", "amount": 181.73804419433577, "unit": "mg/dL"}, {"date": "2015-10-16T12:35:00", "amount": 179.15477304383916, "unit": "mg/dL"}, {"date": "2015-10-16T12:40:00", "amount": 176.22607560460605, "unit": "mg/dL"}, {"date": "2015-10-16T12:45:00", "amount": 172.95524348911732, "unit": "mg/dL"}, {"date": "2015-10-16T12:50:00", "amount": 169.34890670479058, "unit": "mg/dL"}, {"date": "2015-10-16T12:55:00", "amount": 165.50179727544713, "unit": "mg/dL"}, {"date": "2015-10-16T13:00:00", "amount": 161.76379964569085, "unit": "mg/dL"}, {"date": "2015-10-16T13:05:00", "amount": 158.17267323892253, "unit": "mg/dL"}, {"date": "2015-10-16T13:10:00", "amount": 154.724724019594, "unit": "mg/dL"}, {"date": "2015-10-16T13:15:00", "amount": 151.4434826234498, "unit": "mg/dL"}, {"date": "2015-10-16T13:20:00", "amount": 148.33681130846782, "unit": "mg/dL"}, {"date": "2015-10-16T13:25:00", "amount": 145.39838028912266, "unit": "mg/dL"}, {"date": "2015-10-16T13:30:00", "amount": 142.62290995394173, "unit": "mg/dL"}, {"date": "2015-10-16T13:35:00", "amount": 140.06797158224822, "unit": "mg/dL"}, {"date": "2015-10-16T13:40:00", "amount": 137.7128484021019, "unit": "mg/dL"}, {"date": "2015-10-16T13:45:00", "amount": 135.54141436727483, "unit": "mg/dL"}, {"date": "2015-10-16T13:50:00", "amount": 133.5440448592046, "unit": "mg/dL"}, {"date": "2015-10-16T13:55:00", "amount": 131.5428027747512, "unit": "mg/dL"}, {"date": "2015-10-16T14:00:00", "amount": 131.2387009492496, "unit": "mg/dL"}, {"date": "2015-10-16T14:05:00", "amount": 130.98167687086675, "unit": "mg/dL"}, {"date": "2015-10-16T14:10:00", "amount": 130.77233553885304, "unit": "mg/dL"}, {"date": "2015-10-16T14:15:00", "amount": 130.60749011155525, "unit": "mg/dL"}, {"date": "2015-10-16T14:20:00", "amount": 130.4841231903817, "unit": "mg/dL"}, {"date": "2015-10-16T14:25:00", "amount": 130.4006361800759, "unit": "mg/dL"}, {"date": "2015-10-16T14:30:00", "amount": 130.35450801550857, "unit": "mg/dL"}, {"date": "2015-10-16T14:35:00", "amount": 130.3521517822713, "unit": "mg/dL"}] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_no_momentum_output.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_no_momentum_output.json new file mode 100644 index 0000000..2e372eb --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_no_momentum_output.json @@ -0,0 +1 @@ +[{"date": "2015-10-16T09:51:08", "amount": 147.0, "unit": "mg/dL"}, {"date": "2015-10-16T09:55:00", "amount": 147.31844287141575, "unit": "mg/dL"}, {"date": "2015-10-16T10:00:00", "amount": 147.4405645526754, "unit": "mg/dL"}, {"date": "2015-10-16T10:05:00", "amount": 147.391291345801, "unit": "mg/dL"}, {"date": "2015-10-16T10:10:00", "amount": 147.16858437045187, "unit": "mg/dL"}, {"date": "2015-10-16T10:15:00", "amount": 147.40270242548388, "unit": "mg/dL"}, {"date": "2015-10-16T10:20:00", "amount": 147.73163954074346, "unit": "mg/dL"}, {"date": "2015-10-16T10:25:00", "amount": 148.1563528576369, "unit": "mg/dL"}, {"date": "2015-10-16T10:30:00", "amount": 148.69368905652314, "unit": "mg/dL"}, {"date": "2015-10-16T10:35:00", "amount": 149.37224430870094, "unit": "mg/dL"}, {"date": "2015-10-16T10:40:00", "amount": 150.2260003124811, "unit": "mg/dL"}, {"date": "2015-10-16T10:45:00", "amount": 151.28245103919838, "unit": "mg/dL"}, {"date": "2015-10-16T10:50:00", "amount": 152.56843482250991, "unit": "mg/dL"}, {"date": "2015-10-16T10:55:00", "amount": 154.11004843988744, "unit": "mg/dL"}, {"date": "2015-10-16T11:00:00", "amount": 155.93033332095166, "unit": "mg/dL"}, {"date": "2015-10-16T11:05:00", "amount": 158.05438334366448, "unit": "mg/dL"}, {"date": "2015-10-16T11:10:00", "amount": 160.496078349111, "unit": "mg/dL"}, {"date": "2015-10-16T11:15:00", "amount": 163.2772565100948, "unit": "mg/dL"}, {"date": "2015-10-16T11:20:00", "amount": 166.42363597821083, "unit": "mg/dL"}, {"date": "2015-10-16T11:25:00", "amount": 169.7776946537861, "unit": "mg/dL"}, {"date": "2015-10-16T11:30:00", "amount": 172.66938765485244, "unit": "mg/dL"}, {"date": "2015-10-16T11:35:00", "amount": 175.04946089319694, "unit": "mg/dL"}, {"date": "2015-10-16T11:40:00", "amount": 176.9462336132864, "unit": "mg/dL"}, {"date": "2015-10-16T11:45:00", "amount": 178.37707174972755, "unit": "mg/dL"}, {"date": "2015-10-16T11:50:00", "amount": 179.35850238934393, "unit": "mg/dL"}, {"date": "2015-10-16T11:55:00", "amount": 179.9062137711756, "unit": "mg/dL"}, {"date": "2015-10-16T12:00:00", "amount": 180.0357074015875, "unit": "mg/dL"}, {"date": "2015-10-16T12:05:00", "amount": 179.76580208996018, "unit": "mg/dL"}, {"date": "2015-10-16T12:10:00", "amount": 179.07843075920962, "unit": "mg/dL"}, {"date": "2015-10-16T12:15:00", "amount": 177.9874955986132, "unit": "mg/dL"}, {"date": "2015-10-16T12:20:00", "amount": 176.505894105882, "unit": "mg/dL"}, {"date": "2015-10-16T12:25:00", "amount": 174.6426097586433, "unit": "mg/dL"}, {"date": "2015-10-16T12:30:00", "amount": 172.4210928898442, "unit": "mg/dL"}, {"date": "2015-10-16T12:35:00", "amount": 169.8378217393476, "unit": "mg/dL"}, {"date": "2015-10-16T12:40:00", "amount": 166.90912430011448, "unit": "mg/dL"}, {"date": "2015-10-16T12:45:00", "amount": 163.63829218462575, "unit": "mg/dL"}, {"date": "2015-10-16T12:50:00", "amount": 160.031955400299, "unit": "mg/dL"}, {"date": "2015-10-16T12:55:00", "amount": 156.18484597095556, "unit": "mg/dL"}, {"date": "2015-10-16T13:00:00", "amount": 152.44684834119928, "unit": "mg/dL"}, {"date": "2015-10-16T13:05:00", "amount": 148.85572193443096, "unit": "mg/dL"}, {"date": "2015-10-16T13:10:00", "amount": 145.40777271510242, "unit": "mg/dL"}, {"date": "2015-10-16T13:15:00", "amount": 142.12653131895823, "unit": "mg/dL"}, {"date": "2015-10-16T13:20:00", "amount": 139.01986000397625, "unit": "mg/dL"}, {"date": "2015-10-16T13:25:00", "amount": 136.0814289846311, "unit": "mg/dL"}, {"date": "2015-10-16T13:30:00", "amount": 133.30595864945016, "unit": "mg/dL"}, {"date": "2015-10-16T13:35:00", "amount": 130.75102027775665, "unit": "mg/dL"}, {"date": "2015-10-16T13:40:00", "amount": 128.39589709761034, "unit": "mg/dL"}, {"date": "2015-10-16T13:45:00", "amount": 126.22446306278326, "unit": "mg/dL"}, {"date": "2015-10-16T13:50:00", "amount": 124.22709355471304, "unit": "mg/dL"}, {"date": "2015-10-16T13:55:00", "amount": 122.22585147025964, "unit": "mg/dL"}, {"date": "2015-10-16T14:00:00", "amount": 121.92174964475802, "unit": "mg/dL"}, {"date": "2015-10-16T14:05:00", "amount": 121.66472556637518, "unit": "mg/dL"}, {"date": "2015-10-16T14:10:00", "amount": 121.45538423436147, "unit": "mg/dL"}, {"date": "2015-10-16T14:15:00", "amount": 121.29053880706368, "unit": "mg/dL"}, {"date": "2015-10-16T14:20:00", "amount": 121.16717188589013, "unit": "mg/dL"}, {"date": "2015-10-16T14:25:00", "amount": 121.08368487558434, "unit": "mg/dL"}, {"date": "2015-10-16T14:30:00", "amount": 121.037556711017, "unit": "mg/dL"}, {"date": "2015-10-16T14:35:00", "amount": 121.03520047777974, "unit": "mg/dL"}] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_carb_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_carb_input.json new file mode 100644 index 0000000..5eb68ca --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_carb_input.json @@ -0,0 +1,157 @@ +[ + { + "value": 345.692710028959, + "startDate": "2016-03-05T00:05:00-0800", + "unit": "mg/dL" + }, + { + "value": 357.4968709800336, + "startDate": "2016-03-05T00:10:00-0800", + "unit": "mg/dL" + }, + { + "value": 368.2585079256211, + "startDate": "2016-03-05T00:15:00-0800", + "unit": "mg/dL" + }, + { + "value": 377.9776208657216, + "startDate": "2016-03-05T00:20:00-0800", + "unit": "mg/dL" + }, + { + "value": 386.6542098003353, + "startDate": "2016-03-05T00:25:00-0800", + "unit": "mg/dL" + }, + { + "value": 394.2882747294619, + "startDate": "2016-03-05T00:30:00-0800", + "unit": "mg/dL" + }, + { + "value": 400.9472251181222, + "startDate": "2016-03-05T00:35:00-0800", + "unit": "mg/dL" + }, + { + "value": 406.9048840115836, + "startDate": "2016-03-05T00:40:00-0800", + "unit": "mg/dL" + }, + { + "value": 412.1903892699285, + "startDate": "2016-03-05T00:45:00-0800", + "unit": "mg/dL" + }, + { + "value": 416.8037408931565, + "startDate": "2016-03-05T00:50:00-0800", + "unit": "mg/dL" + }, + { + "value": 420.7449388812682, + "startDate": "2016-03-05T00:55:00-0800", + "unit": "mg/dL" + }, + { + "value": 424.013983234263, + "startDate": "2016-03-05T01:00:00-0800", + "unit": "mg/dL" + }, + { + "value": 426.6108739521414, + "startDate": "2016-03-05T01:05:00-0800", + "unit": "mg/dL" + }, + { + "value": 428.5356110349032, + "startDate": "2016-03-05T01:10:00-0800", + "unit": "mg/dL" + }, + { + "value": 429.7881944825484, + "startDate": "2016-03-05T01:15:00-0800", + "unit": "mg/dL" + }, + { + "value": 430.461332114007, + "startDate": "2016-03-05T01:20:00-0800", + "unit": "mg/dL" + }, + { + "value": 430.885657674135, + "startDate": "2016-03-05T01:25:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.0905044962657, + "startDate": "2016-03-05T01:30:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T01:35:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T01:40:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T01:45:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T01:50:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T01:55:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:00:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:05:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:10:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:15:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:20:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:25:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:30:00-0800", + "unit": "mg/dL" + }, + { + "value": 431.1111111111111, + "startDate": "2016-03-05T02:35:00-0800", + "unit": "mg/dL" + } +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_glucose_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_glucose_input.json new file mode 100644 index 0000000..238d951 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_glucose_input.json @@ -0,0 +1,7 @@ +[ + { + "value": 178, + "startDate": "2016-03-05T00:03:14-0800", + "unit": "mg/dL" + } +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_insulin_input.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_insulin_input.json new file mode 100644 index 0000000..af6d5fd --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_insulin_input.json @@ -0,0 +1,272 @@ +[ + { + "value": -262.7985192241191, + "startDate": "2016-03-05T00:05:00-0800", + "unit": "mg/dL" + }, + { + "value": -270.8286417720012, + "startDate": "2016-03-05T00:10:00-0800", + "unit": "mg/dL" + }, + { + "value": -278.8133758667809, + "startDate": "2016-03-05T00:15:00-0800", + "unit": "mg/dL" + }, + { + "value": -286.7284576274255, + "startDate": "2016-03-05T00:20:00-0800", + "unit": "mg/dL" + }, + { + "value": -294.5582531285091, + "startDate": "2016-03-05T00:25:00-0800", + "unit": "mg/dL" + }, + { + "value": -302.5967010990278, + "startDate": "2016-03-05T00:30:00-0800", + "unit": "mg/dL" + }, + { + "value": -310.381943957302, + "startDate": "2016-03-05T00:35:00-0800", + "unit": "mg/dL" + }, + { + "value": -316.6257775805158, + "startDate": "2016-03-05T00:40:00-0800", + "unit": "mg/dL" + }, + { + "value": -322.0179315638999, + "startDate": "2016-03-05T00:45:00-0800", + "unit": "mg/dL" + }, + { + "value": -327.3554504781727, + "startDate": "2016-03-05T00:50:00-0800", + "unit": "mg/dL" + }, + { + "value": -332.5782130464361, + "startDate": "2016-03-05T00:55:00-0800", + "unit": "mg/dL" + }, + { + "value": -337.6793353886575, + "startDate": "2016-03-05T01:00:00-0800", + "unit": "mg/dL" + }, + { + "value": -342.7443165330539, + "startDate": "2016-03-05T01:05:00-0800", + "unit": "mg/dL" + }, + { + "value": -347.7575443621537, + "startDate": "2016-03-05T01:10:00-0800", + "unit": "mg/dL" + }, + { + "value": -352.7116703841897, + "startDate": "2016-03-05T01:15:00-0800", + "unit": "mg/dL" + }, + { + "value": -357.6053319159832, + "startDate": "2016-03-05T01:20:00-0800", + "unit": "mg/dL" + }, + { + "value": -362.4252842472219, + "startDate": "2016-03-05T01:25:00-0800", + "unit": "mg/dL" + }, + { + "value": -367.1551449645343, + "startDate": "2016-03-05T01:30:00-0800", + "unit": "mg/dL" + }, + { + "value": -371.7924658623842, + "startDate": "2016-03-05T01:35:00-0800", + "unit": "mg/dL" + }, + { + "value": -376.3227442729362, + "startDate": "2016-03-05T01:40:00-0800", + "unit": "mg/dL" + }, + { + "value": -380.7370617907551, + "startDate": "2016-03-05T01:45:00-0800", + "unit": "mg/dL" + }, + { + "value": -385.0276152484062, + "startDate": "2016-03-05T01:50:00-0800", + "unit": "mg/dL" + }, + { + "value": -389.187729956454, + "startDate": "2016-03-05T01:55:00-0800", + "unit": "mg/dL" + }, + { + "value": -393.2118729434634, + "startDate": "2016-03-05T02:00:00-0800", + "unit": "mg/dL" + }, + { + "value": -397.0956661959993, + "startDate": "2016-03-05T02:05:00-0800", + "unit": "mg/dL" + }, + { + "value": -400.8358998986266, + "startDate": "2016-03-05T02:10:00-0800", + "unit": "mg/dL" + }, + { + "value": -404.4305456739103, + "startDate": "2016-03-05T02:15:00-0800", + "unit": "mg/dL" + }, + { + "value": -407.8711913354906, + "startDate": "2016-03-05T02:20:00-0800", + "unit": "mg/dL" + }, + { + "value": -411.1887683152845, + "startDate": "2016-03-05T02:25:00-0800", + "unit": "mg/dL" + }, + { + "value": -414.2483273403853, + "startDate": "2016-03-05T02:30:00-0800", + "unit": "mg/dL" + }, + { + "value": -417.237074474721, + "startDate": "2016-03-05T02:35:00-0800", + "unit": "mg/dL" + }, + { + "value": -419.3124522762179, + "startDate": "2016-03-05T02:40:00-0800", + "unit": "mg/dL" + }, + { + "value": -421.1959605419546, + "startDate": "2016-03-05T02:45:00-0800", + "unit": "mg/dL" + }, + { + "value": -422.9152625517117, + "startDate": "2016-03-05T02:50:00-0800", + "unit": "mg/dL" + }, + { + "value": -424.4368282040283, + "startDate": "2016-03-05T02:55:00-0800", + "unit": "mg/dL" + }, + { + "value": -425.7898805619185, + "startDate": "2016-03-05T03:00:00-0800", + "unit": "mg/dL" + }, + { + "value": -426.9822978697815, + "startDate": "2016-03-05T03:05:00-0800", + "unit": "mg/dL" + }, + { + "value": -428.0219419720182, + "startDate": "2016-03-05T03:10:00-0800", + "unit": "mg/dL" + }, + { + "value": -428.9115794359523, + "startDate": "2016-03-05T03:15:00-0800", + "unit": "mg/dL" + }, + { + "value": -429.7051332223956, + "startDate": "2016-03-05T03:20:00-0800", + "unit": "mg/dL" + }, + { + "value": -430.3667421802121, + "startDate": "2016-03-05T03:25:00-0800", + "unit": "mg/dL" + }, + { + "value": -430.9039751398021, + "startDate": "2016-03-05T03:30:00-0800", + "unit": "mg/dL" + }, + { + "value": -431.3241726915655, + "startDate": "2016-03-05T03:35:00-0800", + "unit": "mg/dL" + }, + { + "value": -431.629368308826, + "startDate": "2016-03-05T03:40:00-0800", + "unit": "mg/dL" + }, + { + "value": -431.8675074813201, + "startDate": "2016-03-05T03:45:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.0478052203234, + "startDate": "2016-03-05T03:50:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.128033449624, + "startDate": "2016-03-05T03:55:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1503561538819, + "startDate": "2016-03-05T04:00:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1593079386333, + "startDate": "2016-03-05T04:05:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1559703692269, + "startDate": "2016-03-05T04:10:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1487180963159, + "startDate": "2016-03-05T04:15:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1204246460239, + "startDate": "2016-03-05T04:20:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1333333333329, + "startDate": "2016-03-05T04:25:00-0800", + "unit": "mg/dL" + }, + { + "value": -432.1333333333329, + "startDate": "2016-03-05T04:30:00-0800", + "unit": "mg/dL" + } +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_output.json b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_output.json new file mode 100644 index 0000000..12e9d00 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/glucose_from_effects_non_zero_output.json @@ -0,0 +1,277 @@ +[ + { + "startDate": "2016-03-05T00:03:14-0800", + "unit": "mg/dL", + "value": 178 + }, + { + "startDate": "2016-03-05T00:05:00-0800", + "unit": "mg/dL", + "value": 178 + }, + { + "startDate": "2016-03-05T00:10:00-0800", + "unit": "mg/dL", + "value": 181.7740384031924 + }, + { + "startDate": "2016-03-05T00:15:00-0800", + "unit": "mg/dL", + "value": 184.5509412540002 + }, + { + "startDate": "2016-03-05T00:20:00-0800", + "unit": "mg/dL", + "value": 186.3549724334562 + }, + { + "startDate": "2016-03-05T00:25:00-0800", + "unit": "mg/dL", + "value": 187.2017658669863 + }, + { + "startDate": "2016-03-05T00:30:00-0800", + "unit": "mg/dL", + "value": 186.7973828255941 + }, + { + "startDate": "2016-03-05T00:35:00-0800", + "unit": "mg/dL", + "value": 185.6710903559803 + }, + { + "startDate": "2016-03-05T00:40:00-0800", + "unit": "mg/dL", + "value": 185.3849156262279 + }, + { + "startDate": "2016-03-05T00:45:00-0800", + "unit": "mg/dL", + "value": 185.2782669011887 + }, + { + "startDate": "2016-03-05T00:50:00-0800", + "unit": "mg/dL", + "value": 184.5540996101439 + }, + { + "startDate": "2016-03-05T00:55:00-0800", + "unit": "mg/dL", + "value": 183.2725350299922 + }, + { + "startDate": "2016-03-05T01:00:00-0800", + "unit": "mg/dL", + "value": 181.4404570407656 + }, + { + "startDate": "2016-03-05T01:05:00-0800", + "unit": "mg/dL", + "value": 178.9723666142475 + }, + { + "startDate": "2016-03-05T01:10:00-0800", + "unit": "mg/dL", + "value": 175.8838758679096 + }, + { + "startDate": "2016-03-05T01:15:00-0800", + "unit": "mg/dL", + "value": 172.1823332935188 + }, + { + "startDate": "2016-03-05T01:20:00-0800", + "unit": "mg/dL", + "value": 167.9618093931839 + }, + { + "startDate": "2016-03-05T01:25:00-0800", + "unit": "mg/dL", + "value": 163.5661826220732 + }, + { + "startDate": "2016-03-05T01:30:00-0800", + "unit": "mg/dL", + "value": 159.0411687268914 + }, + { + "startDate": "2016-03-05T01:35:00-0800", + "unit": "mg/dL", + "value": 154.424454443887 + }, + { + "startDate": "2016-03-05T01:40:00-0800", + "unit": "mg/dL", + "value": 149.894176033335 + }, + { + "startDate": "2016-03-05T01:45:00-0800", + "unit": "mg/dL", + "value": 145.4798585155161 + }, + { + "startDate": "2016-03-05T01:50:00-0800", + "unit": "mg/dL", + "value": 141.189305057865 + }, + { + "startDate": "2016-03-05T01:55:00-0800", + "unit": "mg/dL", + "value": 137.0291903498172 + }, + { + "startDate": "2016-03-05T02:00:00-0800", + "unit": "mg/dL", + "value": 133.0050473628078 + }, + { + "startDate": "2016-03-05T02:05:00-0800", + "unit": "mg/dL", + "value": 129.1212541102719 + }, + { + "startDate": "2016-03-05T02:10:00-0800", + "unit": "mg/dL", + "value": 125.3810204076446 + }, + { + "startDate": "2016-03-05T02:15:00-0800", + "unit": "mg/dL", + "value": 121.786374632361 + }, + { + "startDate": "2016-03-05T02:20:00-0800", + "unit": "mg/dL", + "value": 118.3457289707806 + }, + { + "startDate": "2016-03-05T02:25:00-0800", + "unit": "mg/dL", + "value": 115.0281519909867 + }, + { + "startDate": "2016-03-05T02:30:00-0800", + "unit": "mg/dL", + "value": 111.968592965886 + }, + { + "startDate": "2016-03-05T02:35:00-0800", + "unit": "mg/dL", + "value": 108.9798458315503 + }, + { + "startDate": "2016-03-05T02:40:00-0800", + "unit": "mg/dL", + "value": 106.9044680300533 + }, + { + "startDate": "2016-03-05T02:45:00-0800", + "unit": "mg/dL", + "value": 105.0209597643166 + }, + { + "startDate": "2016-03-05T02:50:00-0800", + "unit": "mg/dL", + "value": 103.3016577545596 + }, + { + "startDate": "2016-03-05T02:55:00-0800", + "unit": "mg/dL", + "value": 101.7800921022429 + }, + { + "startDate": "2016-03-05T03:00:00-0800", + "unit": "mg/dL", + "value": 100.4270397443528 + }, + { + "startDate": "2016-03-05T03:05:00-0800", + "unit": "mg/dL", + "value": 99.23462243648973 + }, + { + "startDate": "2016-03-05T03:10:00-0800", + "unit": "mg/dL", + "value": 98.19497833425302 + }, + { + "startDate": "2016-03-05T03:15:00-0800", + "unit": "mg/dL", + "value": 97.30534087031887 + }, + { + "startDate": "2016-03-05T03:20:00-0800", + "unit": "mg/dL", + "value": 96.51178708387565 + }, + { + "startDate": "2016-03-05T03:25:00-0800", + "unit": "mg/dL", + "value": 95.85017812605915 + }, + { + "startDate": "2016-03-05T03:30:00-0800", + "unit": "mg/dL", + "value": 95.31294516646912 + }, + { + "startDate": "2016-03-05T03:35:00-0800", + "unit": "mg/dL", + "value": 94.89274761470577 + }, + { + "startDate": "2016-03-05T03:40:00-0800", + "unit": "mg/dL", + "value": 94.58755199744527 + }, + { + "startDate": "2016-03-05T03:45:00-0800", + "unit": "mg/dL", + "value": 94.34941282495114 + }, + { + "startDate": "2016-03-05T03:50:00-0800", + "unit": "mg/dL", + "value": 94.16911508594782 + }, + { + "startDate": "2016-03-05T03:55:00-0800", + "unit": "mg/dL", + "value": 94.08888685664724 + }, + { + "startDate": "2016-03-05T04:00:00-0800", + "unit": "mg/dL", + "value": 94.06656415238936 + }, + { + "startDate": "2016-03-05T04:05:00-0800", + "unit": "mg/dL", + "value": 94.05761236763797 + }, + { + "startDate": "2016-03-05T04:10:00-0800", + "unit": "mg/dL", + "value": 94.06094993704434 + }, + { + "startDate": "2016-03-05T04:15:00-0800", + "unit": "mg/dL", + "value": 94.06820220995527 + }, + { + "startDate": "2016-03-05T04:20:00-0800", + "unit": "mg/dL", + "value": 94.09649566024731 + }, + { + "startDate": "2016-03-05T04:25:00-0800", + "unit": "mg/dL", + "value": 94.08358697293832 + }, + { + "startDate": "2016-03-05T04:30:00-0800", + "unit": "mg/dL", + "value": 94.08358697293832 + } +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/ice_minus_carb_effect_with_gaps_output.json b/Tests/LoopAlgorithmTests/Fixtures/ice_minus_carb_effect_with_gaps_output.json new file mode 100644 index 0000000..72af9c8 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/ice_minus_carb_effect_with_gaps_output.json @@ -0,0 +1,112 @@ +[ + { + "unit" : "mg\/dL", + "date" : "2018-08-16 01:13:44 +0000", + "value" : -7.9146779853452083 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 01:18:44 +0000", + "value" : -5.9281656186007314 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 01:23:44 +0000", + "value" : 1.2296261019331554 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 01:28:43 +0000", + "value" : 1.4065989188792711 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 02:03:43 +0000", + "value" : 6.0711968367636908 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:13:44 +0000", + "value" : 1.7358764710254451 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:18:44 +0000", + "value" : 0.5343295138009645 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:23:43 +0000", + "value" : 1.4847123043985215 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:28:44 +0000", + "value" : 1.6893378641238699 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:33:43 +0000", + "value" : 6.042541334805744 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:38:43 +0000", + "value" : -9.6914450613633392 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:43:44 +0000", + "value" : 0.58142124976613774 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:48:43 +0000", + "value" : -7.1923719688531209 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:53:44 +0000", + "value" : -2.9941217788457295 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 03:58:43 +0000", + "value" : -11.960291314911359 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:03:43 +0000", + "value" : -1.2512705141339 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:08:43 +0000", + "value" : -0.0014915619982254213 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:13:44 +0000", + "value" : -0.0056015222217293825 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:18:43 +0000", + "value" : 0.0052760433631586778 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:23:44 +0000", + "value" : -0.0051133014060256166 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:28:44 +0000", + "value" : 0.0025222980026429198 + }, + { + "unit" : "mg\/dL", + "date" : "2018-08-16 04:33:43 +0000", + "value" : -1.7950225310593533 + } +] diff --git a/Tests/LoopAlgorithmTests/Fixtures/ice_minus_flat_carb_effect_output.json b/Tests/LoopAlgorithmTests/Fixtures/ice_minus_flat_carb_effect_output.json new file mode 100644 index 0000000..26c5d73 --- /dev/null +++ b/Tests/LoopAlgorithmTests/Fixtures/ice_minus_flat_carb_effect_output.json @@ -0,0 +1,42 @@ +[ + { + "value" : 3.6517114683289944, + "date" : "2018-08-26 00:48:28 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 3.5831456523646765, + "date" : "2018-08-26 00:53:28 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 1.4811396601622884, + "date" : "2018-08-26 00:58:28 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 1.3650980083318285, + "date" : "2018-08-26 01:03:27 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 2.2302804679379356, + "date" : "2018-08-26 01:08:28 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 2.1065985817633233, + "date" : "2018-08-26 01:13:28 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 4.974146844737705, + "date" : "2018-08-26 01:18:27 +0000", + "unit" : "mg\/dL" + }, + { + "value" : 4.8261191053223191, + "date" : "2018-08-26 01:23:28 +0000", + "unit" : "mg\/dL" + } +] diff --git a/Tests/LoopAlgorithmTests/GlucoseMathTests.swift b/Tests/LoopAlgorithmTests/GlucoseMathTests.swift index 8cb1a4f..de9d869 100644 --- a/Tests/LoopAlgorithmTests/GlucoseMathTests.swift +++ b/Tests/LoopAlgorithmTests/GlucoseMathTests.swift @@ -17,15 +17,16 @@ extension XCTestCase { public struct GlucoseFixtureValue: GlucoseSampleValue { public let startDate: Date - public let quantity: LoopQuantity + let quantity: LoopQuantity + public var mgdl: Double { quantity.doubleValue(for: .milligramsPerDeciliter) } public let isDisplayOnly: Bool public let wasUserEntered: Bool public let provenanceIdentifier: String public let condition: GlucoseCondition? - public let trendRate: LoopQuantity? + let trendRate: LoopQuantity? public var syncIdentifier: String? - public init(startDate: Date, quantity: LoopQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, provenanceIdentifier: String?, condition: GlucoseCondition?, trendRate: LoopQuantity?) { + init(startDate: Date, quantity: LoopQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, provenanceIdentifier: String?, condition: GlucoseCondition?, trendRate: LoopQuantity?) { self.startDate = startDate self.quantity = quantity self.isDisplayOnly = isDisplayOnly @@ -49,6 +50,7 @@ final class GlucoseMathTests: XCTestCase { private struct MockGlucoseSample: GlucoseSampleValue { var startDate: Date var quantity: LoopQuantity + var mgdl: Double { quantity.doubleValue(for: .milligramsPerDeciliter) } var provenanceIdentifier: String var isDisplayOnly: Bool var wasUserEntered: Bool diff --git a/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift b/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift index de2053e..54462fa 100644 --- a/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift +++ b/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift @@ -49,7 +49,7 @@ final class LoopAlgorithmTests: XCTestCase { let (input, recommendation) = loadScenario("suspend") - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) XCTAssertEqual(output.recommendation, recommendation) } @@ -62,7 +62,7 @@ final class LoopAlgorithmTests: XCTestCase { let (input, recommendation) = loadScenario("carbs_with_isf_change") - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) XCTAssertEqual(output.recommendation, recommendation) } @@ -73,7 +73,7 @@ final class LoopAlgorithmTests: XCTestCase { input.recommendationType = .manualBolus input.carbEntries.append(FixtureCarbEntry(absorptionTime: .hours(6), startDate: now, quantity: .carbs(50))) - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) XCTAssertEqual(output.activeCarbs, 50) XCTAssertEqual(output.recommendation!.manual!.amount, 5.86, accuracy: 0.01) @@ -100,8 +100,8 @@ final class LoopAlgorithmTests: XCTestCase { ) - let outputA = LoopAlgorithm.run(input: a) - let outputB = LoopAlgorithm.run(input: b) + let outputA = Algorithm.run(input: a) + let outputB = Algorithm.run(input: b) XCTAssertEqual(outputA.activeCarbs, outputB.activeCarbs) XCTAssertEqual(outputA.activeInsulin, outputB.activeInsulin) @@ -148,7 +148,7 @@ final class LoopAlgorithmTests: XCTestCase { FixtureGlucoseSample(startDate: now.addingTimeInterval(.minutes(-3)), quantity: .glucose(145)), ] - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) let carbStatus = output.effects.carbStatus.first! XCTAssertEqual(carbStatus.absorption!.observedProgress.doubleValue(for: .percent), 0.36, accuracy: 0.01) @@ -167,7 +167,7 @@ final class LoopAlgorithmTests: XCTestCase { let url = Bundle.module.url(forResource: "live_capture_input", withExtension: "json", subdirectory: "Fixtures")! let input = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url)) - let prediction = LoopAlgorithm.generatePrediction( + let prediction = Algorithm.generatePrediction( start: input.glucoseHistory.last?.startDate ?? Date(), glucoseHistory: input.glucoseHistory, doses: input.doses, @@ -207,12 +207,12 @@ final class LoopAlgorithmTests: XCTestCase { ] // With a spurious reading over the gradualTransitionsThreshold of 40 mg/dL, momentum and rc are turned off, and the forecast is lower. - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 164.5, accuracy: 0.1) // With the threshold set high, our spurious reading is still considered gradual, and RC and momentum will be used, resulting in a higher forecast. input.gradualTransitionsThreshold = 60 - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 216, accuracy: 0.1) } @@ -225,7 +225,7 @@ final class LoopAlgorithmTests: XCTestCase { let url = Bundle.module.url(forResource: "meal-bolus-isf", withExtension: "json", subdirectory: "Fixtures")! var input = try! decoder.decode(AlgorithmInputFixture.self, from: try! Data(contentsOf: url)) - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) // Should recommend bolus to cover meal XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 274.14, accuracy: 0.1) @@ -241,7 +241,7 @@ final class LoopAlgorithmTests: XCTestCase { ) ) - let output2 = LoopAlgorithm.run(input: input) + let output2 = Algorithm.run(input: input) // 150 mg/dL is the middle of the target range XCTAssertEqual(output2.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 150, accuracy: 0.1) @@ -256,7 +256,7 @@ final class LoopAlgorithmTests: XCTestCase { var input = try! decoder.decode( AlgorithmInputFixture.self, from: try! Data(contentsOf: url)) - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) // Should recommend bolus to cover meal XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 269.15, accuracy: 0.1) @@ -272,7 +272,7 @@ final class LoopAlgorithmTests: XCTestCase { ) ) - let output2 = LoopAlgorithm.run(input: input) + let output2 = Algorithm.run(input: input) // 150 mg/dL is the middle of the target range XCTAssertEqual(output2.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 150, accuracy: 0.1) @@ -292,17 +292,17 @@ final class LoopAlgorithmTests: XCTestCase { // ISF Changing mid-aborption input.sensitivity = [ - AbsoluteScheduleValue(startDate: isfStart, endDate: isfMid, value: .glucose(50)), - AbsoluteScheduleValue(startDate: isfMid, endDate: isfEnd, value: .glucose(100)) + AbsoluteScheduleValue(startDate: isfStart, endDate: isfMid, value: 50), + AbsoluteScheduleValue(startDate: isfMid, endDate: isfEnd, value: 100) ] // With Mid-absorption ISF flag = false - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) XCTAssertEqual(output.effects.insulin.last!.quantity.doubleValue(for: .milligramsPerDeciliter), -50, accuracy: 0.5) // With Mid-absorption ISF flag = true input.useMidAbsorptionISF = true - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) XCTAssertEqual(output.effects.insulin.last!.quantity.doubleValue(for: .milligramsPerDeciliter), -83, accuracy: 0.5) } @@ -325,7 +325,7 @@ final class LoopAlgorithmTests: XCTestCase { // Max activeInsulin = 2 x maxBolus = 16U input.maxBolus = 8 - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) var recommendedBolus = output.recommendation!.automatic?.bolusUnits var activeInsulin = output.activeInsulin! XCTAssertEqual(activeInsulin, 8.0) @@ -333,7 +333,7 @@ final class LoopAlgorithmTests: XCTestCase { // Now try with maxBolus of 4; should not recommend any more insulin, as we're at our max iob input.maxBolus = 4 - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) recommendedBolus = output.recommendation!.automatic?.bolusUnits activeInsulin = output.activeInsulin! XCTAssertEqual(activeInsulin, 8.0) @@ -360,7 +360,7 @@ final class LoopAlgorithmTests: XCTestCase { // Max activeInsulin = 2 x maxBolus = 16U input.maxBolus = 8 - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) var recommendedRate = output.recommendation!.automatic!.basalAdjustment.unitsPerHour var activeInsulin = output.activeInsulin! XCTAssertEqual(activeInsulin, 8.0) @@ -368,7 +368,7 @@ final class LoopAlgorithmTests: XCTestCase { // Now try with maxBolus of 4; should only recommend scheduled basal (1U/hr), as we're at our max iob input.maxBolus = 4 - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) recommendedRate = output.recommendation!.automatic!.basalAdjustment.unitsPerHour activeInsulin = output.activeInsulin! XCTAssertEqual(activeInsulin, 8.0) @@ -384,8 +384,8 @@ final class LoopAlgorithmTests: XCTestCase { // Sensitivity doubles in one hour let isfChangeTime = now.addingTimeInterval(.hours(1)) input.sensitivity = [ - AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-48)), endDate: isfChangeTime, value: .glucose(50)), - AbsoluteScheduleValue(startDate: isfChangeTime, endDate: now.addingTimeInterval(.hours(48)), value: .glucose(100)) + AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-48)), endDate: isfChangeTime, value: 50), + AbsoluteScheduleValue(startDate: isfChangeTime, endDate: now.addingTimeInterval(.hours(48)), value: 100) ] // No insulin or carbs @@ -396,12 +396,12 @@ final class LoopAlgorithmTests: XCTestCase { // Without mid-absorption ISF input.useMidAbsorptionISF = false - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) XCTAssertEqual(2.73, output.recommendation!.manual!.amount, accuracy: 0.01) // With mid-absorption ISF input.useMidAbsorptionISF = true - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) XCTAssertEqual(1.49, output.recommendation!.manual!.amount, accuracy: 0.01) } @@ -425,10 +425,10 @@ final class LoopAlgorithmTests: XCTestCase { // Sensitivity doesn't cover start of basal dose input.sensitivity = [ - AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), value: .glucose(50)), + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), value: 50), ] - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) guard case .failure(AlgorithmError.sensitivityTimelineStartsTooLate) = output.recommendationResult else { XCTFail("Expected sensitivityTimelineStartsTooLate failure") return @@ -439,10 +439,10 @@ final class LoopAlgorithmTests: XCTestCase { AbsoluteScheduleValue( startDate: basalStart.dateFlooredToTimeInterval(GlucoseMath.defaultDelta), endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), - value: .glucose(50) + value: 50 ), ] - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) guard case .failure(AlgorithmError.sensitivityTimelineEndsTooEarly) = output.recommendationResult else { XCTFail("Expected sensitivityTimelineEndsTooEarly failure") return @@ -454,7 +454,7 @@ final class LoopAlgorithmTests: XCTestCase { start: input.predictionStart, duration: input.recommendationInsulinModel.effectDuration ) - let neededISFInterval = LoopAlgorithm.timelineIntervalForSensitivity( + let neededISFInterval = Algorithm.timelineIntervalForSensitivity( doses: input.doses, glucoseHistoryStart: input.glucoseHistory.first!.startDate, recommendationEffectInterval: recommendationEffectInterval @@ -463,10 +463,10 @@ final class LoopAlgorithmTests: XCTestCase { AbsoluteScheduleValue( startDate: neededISFInterval.start, endDate: neededISFInterval.end, - value: .glucose(50) + value: 50 ) ] - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) guard case .success = output.recommendationResult else { XCTFail("Expected recommendationResult success") return @@ -485,7 +485,7 @@ final class LoopAlgorithmTests: XCTestCase { let entryDate = timelinesStart.addingTimeInterval(.minutes(-30)) input.carbEntries.insert(FixtureCarbEntry(absorptionTime: .hours(3), startDate: entryDate, quantity: .carbs(20)), at: 0) - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) guard case .failure(AlgorithmError.carbRatioOrSensitivityStartsAfterCarbEntry) = output.recommendationResult else { XCTFail("Expected carbRatioOrSensitivityStartsAfterCarbEntry failure") return @@ -494,7 +494,7 @@ final class LoopAlgorithmTests: XCTestCase { // Extend both timelines back to the entry let firstCarbRatio = input.carbRatio.first! input.carbRatio.insert(AbsoluteScheduleValue(startDate: entryDate, endDate: firstCarbRatio.startDate, value: firstCarbRatio.value), at: 0) - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) guard case .failure(AlgorithmError.carbRatioOrSensitivityStartsAfterCarbEntry) = output.recommendationResult else { XCTFail("Expected failure while ISF still starts after the entry") return @@ -502,7 +502,7 @@ final class LoopAlgorithmTests: XCTestCase { let firstSensitivity = input.sensitivity.first! input.sensitivity.insert(AbsoluteScheduleValue(startDate: entryDate, endDate: firstSensitivity.startDate, value: firstSensitivity.value), at: 0) - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) guard case .success = output.recommendationResult else { XCTFail("Expected recommendationResult success") return @@ -532,12 +532,12 @@ final class LoopAlgorithmTests: XCTestCase { AbsoluteScheduleValue( startDate: basalStart.dateFlooredToTimeInterval(GlucoseMath.defaultDelta), endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), - value: .glucose(50) + value: 50 ), ] input.useMidAbsorptionISF = true - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) guard case .failure(AlgorithmError.sensitivityTimelineEndsTooEarly) = output.recommendationResult else { XCTFail("Expected sensitivityTimelineEndsTooEarly failure") return @@ -567,7 +567,7 @@ final class LoopAlgorithmTests: XCTestCase { FixtureGlucoseSample(startDate: now.addingTimeInterval(.minutes(-0)), quantity: .glucose(120)), ] - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) let basalAdjustment = output.recommendation!.automatic!.basalAdjustment XCTAssertEqual(basalAdjustment.unitsPerHour, 4.94, accuracy: 0.01) @@ -590,7 +590,7 @@ final class LoopAlgorithmTests: XCTestCase { // Manual recommendation does not cut off doses in progress, like a running temp basal input.recommendationType = .manualBolus - output = LoopAlgorithm.run(input: input) + output = Algorithm.run(input: input) XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 105, accuracy: 0.5) } } diff --git a/Tests/LoopAlgorithmTests/LoopMathEffectsTests.swift b/Tests/LoopAlgorithmTests/LoopMathEffectsTests.swift new file mode 100644 index 0000000..9e8c089 --- /dev/null +++ b/Tests/LoopAlgorithmTests/LoopMathEffectsTests.swift @@ -0,0 +1,522 @@ +// +// LoopMathEffectsTests.swift +// LoopAlgorithm +// +// Created by Nathan Racklyeft on 3/2/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import XCTest +@testable import LoopAlgorithm + + +typealias RecentGlucoseValue = PredictedGlucoseValue + + +class LoopMathEffectsTests: XCTestCase { + + func loadGlucoseEffectFixture(_ resourceName: String, formatter: ISO8601DateFormatter) -> [GlucoseEffect] { + let fixture: [JSONDictionary] = loadFixture(resourceName) + + return fixture.map { + return GlucoseEffect(startDate: formatter.date(from: $0["date"] as! String)!, quantity: LoopQuantity(unit: LoopUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double)) + } + } + + func loadGlucoseEffectFixture(_ resourceName: String, formatter: DateFormatter) -> [GlucoseEffect] { + let fixture: [JSONDictionary] = loadFixture(resourceName) + + return fixture.map { + return GlucoseEffect(startDate: formatter.date(from: $0["date"] as! String)!, quantity: LoopQuantity(unit: LoopUnit(from: $0["unit"] as! String), doubleValue: $0["value"] as! Double)) + } + } + + private func printFixture(_ glucoseEffect: [GlucoseEffect]) { + let unit = LoopUnit.milligramsPerDeciliter + + print("\n\n") + print(String(data: try! JSONSerialization.data( + withJSONObject: glucoseEffect.map({ (value) -> [String: Any] in + return [ + "date": String(describing: value.startDate), + "value": value.quantity.doubleValue(for: unit), + "unit": unit.unitString + ] + }), + options: .prettyPrinted), encoding: .utf8)!) + print("\n\n") + } + + func loadSampleValueFixture(_ resourceName: String) -> [(startDate: Date, quantity: LoopQuantity)] { + let fixture: [JSONDictionary] = loadFixture(resourceName) + let dateFormatter = ISO8601DateFormatter() + + return fixture.map { + (dateFormatter.date(from: $0["startDate"] as! String)!, LoopQuantity(unit: LoopUnit(from: $0["unit"] as! String), doubleValue: $0["value"] as! Double)) + } + } + + func loadGlucoseHistoryFixture(_ resourceName: String) -> RecentGlucoseValue { + let fixture: [JSONDictionary] = loadFixture(resourceName) + let dateFormatter = ISO8601DateFormatter.localTimeDate() + + return fixture.map { + return RecentGlucoseValue(startDate: dateFormatter.date(from: $0["display_time"] as! String)!, quantity: LoopQuantity(unit: LoopUnit.milligramsPerDeciliter, doubleValue:$0["glucose"] as! Double)) + }.first! + } + + func loadGlucoseValueFixture(_ resourceName: String) -> [PredictedGlucoseValue] { + let fixture: [JSONDictionary] = loadFixture(resourceName) + let dateFormatter = ISO8601DateFormatter.localTimeDate() + + return fixture.map { + return PredictedGlucoseValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: LoopQuantity(unit: LoopUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double)) + } + } + + lazy var carbEffect: [GlucoseEffect] = { + return self.loadGlucoseEffectFixture("glucose_from_effects_carb_effect_input", formatter: ISO8601DateFormatter.localTimeDate()) + }() + + lazy var insulinEffect: [GlucoseEffect] = { + return self.loadGlucoseEffectFixture("glucose_from_effects_insulin_effect_input", formatter: ISO8601DateFormatter.localTimeDate()) + }() + + func testPredictGlucoseNoMomentum() { + let glucose = loadGlucoseHistoryFixture("glucose_from_effects_glucose_input") + + let expected = loadGlucoseValueFixture("glucose_from_effects_no_momentum_output") + + let calculated = LoopMath.predictGlucose(startingAt: glucose, effects: carbEffect, insulinEffect) + + XCTAssertEqual(expected.count, calculated.count) + + for (expected, calculated) in zip(expected, calculated) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) + } + } + + func testPredictGlucoseFlatMomentum() { + let glucose = loadGlucoseHistoryFixture("glucose_from_effects_momentum_flat_glucose_input") + let momentum = loadGlucoseEffectFixture("glucose_from_effects_momentum_flat_input", formatter: ISO8601DateFormatter.localTimeDate()) + let expected = loadGlucoseValueFixture("glucose_from_effects_momentum_flat_output") + + let calculated = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: carbEffect, insulinEffect) + + XCTAssertEqual(expected.count, calculated.count) + + for (expected, calculated) in zip(expected, calculated) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) + } + } + + func testPredictGlucoseUpMomentum() { + let glucose = loadGlucoseHistoryFixture("glucose_from_effects_glucose_input") + let momentum = loadGlucoseEffectFixture("glucose_from_effects_momentum_up_input", formatter: ISO8601DateFormatter.localTimeDate()) + let expected = loadGlucoseValueFixture("glucose_from_effects_momentum_up_output") + + let calculated = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: carbEffect, insulinEffect) + + XCTAssertEqual(expected.count, calculated.count) + + for (expected, calculated) in zip(expected, calculated) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) + } + } + + func testPredictGlucoseDownMomentum() { + let glucose = loadGlucoseHistoryFixture("glucose_from_effects_glucose_input") + let momentum = loadGlucoseEffectFixture("glucose_from_effects_momentum_down_input", formatter: ISO8601DateFormatter.localTimeDate()) + let expected = loadGlucoseValueFixture("glucose_from_effects_momentum_down_output") + + let calculated = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: carbEffect, insulinEffect) + + XCTAssertEqual(expected.count, calculated.count) + + for (expected, calculated) in zip(expected, calculated) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) + } + } + + func testPredictGlucoseBlendMomentum() { + let glucose = loadGlucoseHistoryFixture("glucose_from_effects_momentum_blend_glucose_input") + let momentum = loadGlucoseEffectFixture("glucose_from_effects_momentum_blend_momentum_input", formatter: ISO8601DateFormatter.localTimeDate()) + let insulinEffect = loadGlucoseEffectFixture("glucose_from_effects_momentum_blend_insulin_effect_input", formatter: ISO8601DateFormatter.localTimeDate()) + let expected = loadGlucoseValueFixture("glucose_from_effects_momentum_blend_output") + + let calculated = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: insulinEffect) + + XCTAssertEqual(expected.count, calculated.count) + + for (expected, calculated) in zip(expected, calculated) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) + } + } + + func testPredictGlucoseStartingEffectsNonZero() { + let glucose = loadSampleValueFixture("glucose_from_effects_non_zero_glucose_input").first! + let insulinEffect = loadSampleValueFixture("glucose_from_effects_non_zero_insulin_input").map { + GlucoseEffect(startDate: $0.startDate, quantity: $0.quantity) + } + let carbEffect = loadSampleValueFixture("glucose_from_effects_non_zero_carb_input").map { + GlucoseEffect(startDate: $0.startDate, quantity: $0.quantity) + } + let expected = loadSampleValueFixture("glucose_from_effects_non_zero_output").map { + GlucoseEffect(startDate: $0.startDate, quantity: $0.quantity) + } + + let calculated = LoopMath.predictGlucose(startingAt: RecentGlucoseValue(startDate: glucose.startDate, quantity: glucose.quantity), + effects: insulinEffect, carbEffect + ) + + XCTAssertEqual(expected.count, calculated.count) + + for (expected, calculated) in zip(expected, calculated) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: Double(Float.ulpOfOne)) + } + } + + func testDecayEffect() { + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let glucoseDate = calendar.date(from: DateComponents(year: 2016, month: 2, day: 1, hour: 10, minute: 13, second: 20))! + let unit = LoopUnit.milligramsPerDeciliter + let glucose = FixtureGlucoseSample(startDate: glucoseDate, mgdl: 100) + + var startingEffect = LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 2) + + var effects = glucose.decayEffect(atRate: startingEffect, for: .minutes(30)) + + // decayEffect is now a continuous function of the (unaligned) sample + // timestamp: the 10:13:20 glucose sits 1m40s before the first 5-min grid + // point, so the values interpolate rather than snapping to the grid. + // (testDecayEffectWithEvenGlucose, aligned to :00, still gives the integer + // grid values.) Irrational decimals, so compare with accuracy. + func assertEqual(_ expected: [Double], _ actual: [Double], line: UInt = #line) { + XCTAssertEqual(expected.count, actual.count, line: line) + for (e, a) in zip(expected, actual) { XCTAssertEqual(e, a, accuracy: 1e-6, line: line) } + } + + assertEqual([100, 103.555556, 112.888889, 120.222222, 125.555556, 128.888889, 130.222222], + effects.map { $0.quantity.doubleValue(for: unit) }) + + let startDate = effects.first!.startDate + XCTAssertEqual([0, 5, 10, 15, 20, 25, 30], effects.map { $0.startDate.timeIntervalSince(startDate).minutes }) + + startingEffect = LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -0.5) + effects = glucose.decayEffect(atRate: startingEffect, for: .minutes(30)) + assertEqual([100, 99.111111, 96.777778, 94.944444, 93.611111, 92.777778, 92.444444], + effects.map { $0.quantity.doubleValue(for: unit) }) + } + + func testDecayEffectWithEvenGlucose() { + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let glucoseDate = calendar.date(from: DateComponents(year: 2016, month: 2, day: 1, hour: 10, minute: 15, second: 0))! + let unit = LoopUnit.milligramsPerDeciliter + let glucose = FixtureGlucoseSample(startDate: glucoseDate, mgdl: 100) + + var startingEffect = LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 2) + + var effects = glucose.decayEffect(atRate: startingEffect, for: .minutes(30)) + + XCTAssertEqual([100, 110, 118, 124, 128, 130], effects.map { $0.quantity.doubleValue(for: unit) }) + + let startDate = effects.first!.startDate + XCTAssertEqual([0, 5, 10, 15, 20, 25], effects.map { $0.startDate.timeIntervalSince(startDate).minutes }) + + startingEffect = LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -0.5) + effects = glucose.decayEffect(atRate: startingEffect, for: .minutes(30)) + XCTAssertEqual([100, 97.5, 95.5, 94, 93, 92.5], effects.map { $0.quantity.doubleValue(for: unit) }) + } + + func testSubtractingCarbEffectFromICEWithGaps() { + let perMinute = LoopUnit.milligramsPerDeciliterPerMinute + let mgdl = LoopUnit.milligramsPerDeciliter + + let formatter = DateFormatter.descriptionFormatter + let f = { (input) in + return formatter.date(from: input)! + } + + let insulinCounteractionEffects = [ + GlucoseEffectVelocity(startDate: f("2018-08-16 01:03:43 +0000"), endDate: f("2018-08-16 01:08:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.0063630385383878375)), + GlucoseEffectVelocity(startDate: f("2018-08-16 01:08:43 +0000"), endDate: f("2018-08-16 01:13:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.3798225216554212)), + GlucoseEffectVelocity(startDate: f("2018-08-16 01:13:44 +0000"), endDate: f("2018-08-16 01:18:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.5726970790754702)), + GlucoseEffectVelocity(startDate: f("2018-08-16 01:18:44 +0000"), endDate: f("2018-08-16 01:23:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.7936837738660198)), + GlucoseEffectVelocity(startDate: f("2018-08-16 01:23:44 +0000"), endDate: f("2018-08-16 01:28:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 1.2143200509835315)), + GlucoseEffectVelocity(startDate: f("2018-08-16 01:28:43 +0000"), endDate: f("2018-08-16 02:03:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 1.214239367352738)), + GlucoseEffectVelocity(startDate: f("2018-08-16 02:03:43 +0000"), endDate: f("2018-08-16 03:13:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.64964443000756)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:18:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.40933503856266396)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:18:44 +0000"), endDate: f("2018-08-16 03:23:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.5994115966821696)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:23:43 +0000"), endDate: f("2018-08-16 03:28:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.640336708627245)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:28:44 +0000"), endDate: f("2018-08-16 03:33:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 1.5109774027636143)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:33:43 +0000"), endDate: f("2018-08-16 03:38:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -1.6358198764701966)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:38:43 +0000"), endDate: f("2018-08-16 03:43:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.4187533857556986)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:43:44 +0000"), endDate: f("2018-08-16 03:48:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -1.136005257968153)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:48:43 +0000"), endDate: f("2018-08-16 03:53:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.29635521996668046)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:53:44 +0000"), endDate: f("2018-08-16 03:58:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -1.070285990192832)), + GlucoseEffectVelocity(startDate: f("2018-08-16 03:58:43 +0000"), endDate: f("2018-08-16 04:03:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.25025410282677996)), + GlucoseEffectVelocity(startDate: f("2018-08-16 04:03:43 +0000"), endDate: f("2018-08-16 04:08:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.5598990284715561)), + GlucoseEffectVelocity(startDate: f("2018-08-16 04:08:43 +0000"), endDate: f("2018-08-16 04:13:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 1.9616378142801167)), + GlucoseEffectVelocity(startDate: f("2018-08-16 04:13:44 +0000"), endDate: f("2018-08-16 04:18:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 1.7593854114682483)), + GlucoseEffectVelocity(startDate: f("2018-08-16 04:18:43 +0000"), endDate: f("2018-08-16 04:23:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.5467358931981837)), + GlucoseEffectVelocity(startDate: f("2018-08-16 04:23:44 +0000"), endDate: f("2018-08-16 04:28:44 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.9335047268082058)), + GlucoseEffectVelocity(startDate: f("2018-08-16 04:28:44 +0000"), endDate: f("2018-08-16 04:33:43 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.2823274349191665)), + ] + + let carbEffects = [ + GlucoseEffect(startDate: f("2018-08-16 01:00:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 69.38642455065255)), + GlucoseEffect(startDate: f("2018-08-16 01:05:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 69.38642455065255)), + GlucoseEffect(startDate: f("2018-08-16 01:10:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 72.18741125500856)), + GlucoseEffect(startDate: f("2018-08-16 01:15:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 82.00120184863087)), + GlucoseEffect(startDate: f("2018-08-16 01:20:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 90.79285286260895)), + GlucoseEffect(startDate: f("2018-08-16 01:25:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 93.5316456300059)), + GlucoseEffect(startDate: f("2018-08-16 01:30:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 98.19664696604428)), + GlucoseEffect(startDate: f("2018-08-16 01:35:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 98.5800323225078)), + GlucoseEffect(startDate: f("2018-08-16 01:40:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 100.09237800152016)), + GlucoseEffect(startDate: f("2018-08-16 01:45:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 101.60472368053252)), + GlucoseEffect(startDate: f("2018-08-16 01:50:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 103.11706935954487)), + GlucoseEffect(startDate: f("2018-08-16 01:55:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 104.6294150385572)), + GlucoseEffect(startDate: f("2018-08-16 02:00:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 129.38642455065255)), + GlucoseEffect(startDate: f("2018-08-16 02:05:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 129.38642455065255)), + GlucoseEffect(startDate: f("2018-08-16 02:10:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 132.18741125500856)), + GlucoseEffect(startDate: f("2018-08-16 02:15:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 142.00120184863087)), + GlucoseEffect(startDate: f("2018-08-16 02:20:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 150.79285286260895)), + GlucoseEffect(startDate: f("2018-08-16 02:25:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 153.5316456300059)), + GlucoseEffect(startDate: f("2018-08-16 02:30:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 158.19664696604428)), + GlucoseEffect(startDate: f("2018-08-16 02:35:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 158.5800323225078)), + GlucoseEffect(startDate: f("2018-08-16 02:40:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 160.09237800152016)), + GlucoseEffect(startDate: f("2018-08-16 02:45:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 161.60472368053252)), + GlucoseEffect(startDate: f("2018-08-16 02:50:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 163.11706935954487)), + GlucoseEffect(startDate: f("2018-08-16 02:55:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 164.6294150385572)), + GlucoseEffect(startDate: f("2018-08-16 03:00:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 166.14176071756955)), + GlucoseEffect(startDate: f("2018-08-16 03:05:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 167.65410639658188)), + GlucoseEffect(startDate: f("2018-08-16 03:10:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 169.16645207559424)), + GlucoseEffect(startDate: f("2018-08-16 03:15:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 170.6787977546066)), + GlucoseEffect(startDate: f("2018-08-16 03:20:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 172.19114343361895)), + GlucoseEffect(startDate: f("2018-08-16 03:25:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 173.70348911263127)), + GlucoseEffect(startDate: f("2018-08-16 03:30:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 175.21583479164363)), + GlucoseEffect(startDate: f("2018-08-16 03:35:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 176.72818047065596)), + GlucoseEffect(startDate: f("2018-08-16 03:40:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 178.2405261496683)), + GlucoseEffect(startDate: f("2018-08-16 03:45:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 179.75287182868067)), + GlucoseEffect(startDate: f("2018-08-16 03:50:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 181.26521750769302)), + GlucoseEffect(startDate: f("2018-08-16 03:55:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 182.77756318670535)), + GlucoseEffect(startDate: f("2018-08-16 04:00:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 189.38642455065255)), + GlucoseEffect(startDate: f("2018-08-16 04:05:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 189.38642455065255)), + GlucoseEffect(startDate: f("2018-08-16 04:10:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 192.18741125500856)), + GlucoseEffect(startDate: f("2018-08-16 04:15:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 202.00120184863087)), + GlucoseEffect(startDate: f("2018-08-16 04:20:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 210.79285286260895)), + GlucoseEffect(startDate: f("2018-08-16 04:25:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 213.5316456300059)), + GlucoseEffect(startDate: f("2018-08-16 04:30:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 218.19664696604428)), + GlucoseEffect(startDate: f("2018-08-16 04:35:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 218.5800323225078)), + GlucoseEffect(startDate: f("2018-08-16 04:40:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 220.09237800152016)), + GlucoseEffect(startDate: f("2018-08-16 04:45:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 221.60472368053252)), + GlucoseEffect(startDate: f("2018-08-16 04:50:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 223.11706935954487)), + GlucoseEffect(startDate: f("2018-08-16 04:55:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 224.6294150385572)), + GlucoseEffect(startDate: f("2018-08-16 05:00:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 226.14176071756955)), + GlucoseEffect(startDate: f("2018-08-16 05:05:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 227.65410639658188)), + GlucoseEffect(startDate: f("2018-08-16 05:10:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 229.16645207559424)), + GlucoseEffect(startDate: f("2018-08-16 05:15:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 230.6787977546066)), + GlucoseEffect(startDate: f("2018-08-16 05:20:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 232.19114343361895)), + GlucoseEffect(startDate: f("2018-08-16 05:25:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 233.70348911263127)), + GlucoseEffect(startDate: f("2018-08-16 05:30:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 235.21583479164363)), + GlucoseEffect(startDate: f("2018-08-16 05:35:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 236.72818047065596)), + GlucoseEffect(startDate: f("2018-08-16 05:40:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 238.2405261496683)), + GlucoseEffect(startDate: f("2018-08-16 05:45:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 239.75287182868067)), + GlucoseEffect(startDate: f("2018-08-16 05:50:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 241.26521750769302)), + GlucoseEffect(startDate: f("2018-08-16 05:55:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 242.77756318670535)), + GlucoseEffect(startDate: f("2018-08-16 06:00:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 244.28990886571768)), + GlucoseEffect(startDate: f("2018-08-16 06:05:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 245.80225454473003)), + GlucoseEffect(startDate: f("2018-08-16 06:10:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 247.3146002237424)), + GlucoseEffect(startDate: f("2018-08-16 06:15:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 248.82694590275474)), + GlucoseEffect(startDate: f("2018-08-16 06:20:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 250.33929158176707)), + GlucoseEffect(startDate: f("2018-08-16 06:25:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 251.85163726077943)), + GlucoseEffect(startDate: f("2018-08-16 06:30:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 253.16666666666669)), + GlucoseEffect(startDate: f("2018-08-16 06:35:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 253.16666666666669)), + GlucoseEffect(startDate: f("2018-08-16 06:40:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 253.16666666666669)), + ] + + let calculated = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: .minutes(5)) + + let expected = loadGlucoseEffectFixture("ice_minus_carb_effect_with_gaps_output", formatter: DateFormatter.descriptionFormatter) + + XCTAssertSampleValueEqual(expected, calculated) + } + + func testSubtractingFlatCarbEffectFromICE() { + let perMinute = LoopUnit.milligramsPerDeciliterPerMinute + let mgdl = LoopUnit.milligramsPerDeciliter + + let formatter = DateFormatter.descriptionFormatter + let f = { (input) in + return formatter.date(from: input)! + } + + let insulinCounteractionEffects = [ + GlucoseEffectVelocity(startDate: f("2018-08-26 00:23:27 +0000"), endDate: f("2018-08-26 00:28:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.3711911901542791)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:28:28 +0000"), endDate: f("2018-08-26 00:33:27 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.4382943196158106)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:33:27 +0000"), endDate: f("2018-08-26 00:38:27 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.24797050925219474)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:38:27 +0000"), endDate: f("2018-08-26 00:43:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: -0.05800368381887202)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:43:28 +0000"), endDate: f("2018-08-26 00:48:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.7303422936657988)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:48:28 +0000"), endDate: f("2018-08-26 00:53:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.7166291304729353)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:53:28 +0000"), endDate: f("2018-08-26 00:58:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.2962279320324577)), + GlucoseEffectVelocity(startDate: f("2018-08-26 00:58:28 +0000"), endDate: f("2018-08-26 01:03:27 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.2730196016663657)), + GlucoseEffectVelocity(startDate: f("2018-08-26 01:03:27 +0000"), endDate: f("2018-08-26 01:08:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.44605609358758713)), + GlucoseEffectVelocity(startDate: f("2018-08-26 01:08:28 +0000"), endDate: f("2018-08-26 01:13:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.42131971635266463)), + GlucoseEffectVelocity(startDate: f("2018-08-26 01:13:28 +0000"), endDate: f("2018-08-26 01:18:27 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.9948293689475411)), + GlucoseEffectVelocity(startDate: f("2018-08-26 01:18:27 +0000"), endDate: f("2018-08-26 01:23:28 +0000"), quantity: LoopQuantity(unit: perMinute, doubleValue: 0.9652238210644638)), + ] + + let carbEffects = [ + GlucoseEffect(startDate: f("2018-08-26 00:45:00 +0000"), quantity: LoopQuantity(unit: mgdl, doubleValue: 385.8235294117647)) + ] + + let calculated = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: .minutes(5)) + + let expected = loadGlucoseEffectFixture("ice_minus_flat_carb_effect_output", formatter: DateFormatter.descriptionFormatter) + + XCTAssertSampleValueEqual(expected, calculated) + } + + func testCombinedSumsWithGaps() { + let input = loadGlucoseEffectFixture("ice_minus_carb_effect_with_gaps_output", formatter: DateFormatter.descriptionFormatter) + let unit = LoopUnit.milligramsPerDeciliter + + let formatter = DateFormatter.descriptionFormatter + let f = { (input) in + return formatter.date(from: input)! + } + + let expected = [ + GlucoseChange(startDate: f("2018-08-16 01:13:44 +0000"), endDate: f("2018-08-16 01:13:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -7.914677985345208)), + GlucoseChange(startDate: f("2018-08-16 01:13:44 +0000"), endDate: f("2018-08-16 01:18:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -13.84284360394594)), + GlucoseChange(startDate: f("2018-08-16 01:13:44 +0000"), endDate: f("2018-08-16 01:23:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -12.613217502012784)), + GlucoseChange(startDate: f("2018-08-16 01:13:44 +0000"), endDate: f("2018-08-16 01:28:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -11.206618583133514)), + GlucoseChange(startDate: f("2018-08-16 02:03:43 +0000"), endDate: f("2018-08-16 02:03:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 6.071196836763691)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:13:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 1.735876471025445)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:18:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 2.2702059848264096)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:23:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 3.754918289224931)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:28:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 5.444256153348801)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:33:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 11.486797488154545)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:38:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 1.7953524267912058)), + GlucoseChange(startDate: f("2018-08-16 03:13:44 +0000"), endDate: f("2018-08-16 03:43:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 2.376773676557343)), + GlucoseChange(startDate: f("2018-08-16 03:18:44 +0000"), endDate: f("2018-08-16 03:48:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -6.551474763321222)), + GlucoseChange(startDate: f("2018-08-16 03:28:44 +0000"), endDate: f("2018-08-16 03:53:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -11.56463836036644)), + GlucoseChange(startDate: f("2018-08-16 03:28:44 +0000"), endDate: f("2018-08-16 03:58:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -23.524929675277797)), + GlucoseChange(startDate: f("2018-08-16 03:33:43 +0000"), endDate: f("2018-08-16 04:03:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -26.46553805353557)), + GlucoseChange(startDate: f("2018-08-16 03:38:43 +0000"), endDate: f("2018-08-16 04:08:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -32.50957095033954)), + GlucoseChange(startDate: f("2018-08-16 03:43:44 +0000"), endDate: f("2018-08-16 04:13:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -22.823727411197925)), + GlucoseChange(startDate: f("2018-08-16 03:48:43 +0000"), endDate: f("2018-08-16 04:18:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -23.399872617600906)), + GlucoseChange(startDate: f("2018-08-16 03:53:44 +0000"), endDate: f("2018-08-16 04:23:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -16.21261395015381)), + GlucoseChange(startDate: f("2018-08-16 04:03:43 +0000"), endDate: f("2018-08-16 04:28:44 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -1.2556785583940788)), + GlucoseChange(startDate: f("2018-08-16 04:03:43 +0000"), endDate: f("2018-08-16 04:33:43 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: -3.0507010894534323)) + ] + + let calculated = input.combinedSums(of: .minutes(30)) + + XCTAssertSampleValueEqual(expected, calculated) + } + + + func testNetEffect() { + let formatter = DateFormatter.descriptionFormatter + let f = { (input) in + return formatter.date(from: input)! + } + + let unit = LoopUnit.milligramsPerDeciliter + + let input = [ + GlucoseEffect(startDate: f("2018-08-16 01:00:00 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 25)), + GlucoseEffect(startDate: f("2018-08-16 01:05:00 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 26)), + GlucoseEffect(startDate: f("2018-08-16 01:10:00 +0000"), quantity: LoopQuantity(unit: unit, doubleValue: 27)) + ] + + let calculated = input.netEffect() + + let expected = GlucoseChange( + startDate: f("2018-08-16 01:00:00 +0000"), + endDate: f("2018-08-16 01:10:00 +0000"), + quantity: LoopQuantity(unit: unit, doubleValue: 2)) + + XCTAssertEqual(expected, calculated) + } + + func testDateRange() { + let formatter = DateFormatter.descriptionFormatter + let f = { (input) in + return formatter.date(from: input)! + } + + let expected = [ + f("2018-08-16 01:00:00 +0000"), + f("2018-08-16 01:05:00 +0000"), + f("2018-08-16 01:10:00 +0000"), + f("2018-08-16 01:15:00 +0000"), + f("2018-08-16 01:20:00 +0000"), + f("2018-08-16 01:25:00 +0000"), + f("2018-08-16 01:30:00 +0000"), + f("2018-08-16 01:35:00 +0000"), + f("2018-08-16 01:40:00 +0000"), + f("2018-08-16 01:45:00 +0000"), + f("2018-08-16 01:50:00 +0000"), + f("2018-08-16 01:55:00 +0000"), + f("2018-08-16 02:00:00 +0000"), + f("2018-08-16 02:05:00 +0000"), + f("2018-08-16 02:10:00 +0000"), + f("2018-08-16 02:15:00 +0000"), + f("2018-08-16 02:20:00 +0000"), + f("2018-08-16 02:25:00 +0000"), + f("2018-08-16 02:30:00 +0000"), + f("2018-08-16 02:35:00 +0000"), + f("2018-08-16 02:40:00 +0000"), + f("2018-08-16 02:45:00 +0000"), + f("2018-08-16 02:50:00 +0000"), + f("2018-08-16 02:55:00 +0000"), + f("2018-08-16 03:00:00 +0000"), + f("2018-08-16 03:05:00 +0000"), + f("2018-08-16 03:10:00 +0000") + ] + + let roundedStartDate = f("2018-08-16 01:00:00 +0000") + let unroundedStartDate = f("2018-08-16 01:04:13 +0000") + + let roundedEndDate = f("2018-08-16 03:10:00 +0000") + let unroundedEndDate = f("2018-08-16 03:06:29 +0000") + + let roundedOutput = LoopMath.simulationDateRange(from: roundedStartDate, to: roundedEndDate, delta: .minutes(5)) + XCTAssertEqual(expected, roundedOutput) + + let unroundedOutput = LoopMath.simulationDateRange(from: unroundedStartDate, to: unroundedEndDate, delta: .minutes(5)) + XCTAssertEqual(expected, unroundedOutput) + + let differentDeltaExpected = expected + .enumerated() + .compactMap { $0.offset.isMultiple(of: 2) ? $0.element : nil } + let differentDeltaOutput = LoopMath.simulationDateRange(from: unroundedStartDate, to: unroundedEndDate, delta: .minutes(10)) + XCTAssertEqual(differentDeltaExpected, differentDeltaOutput) + } +} + +func XCTAssertSampleValueEqual(_ expression1: [T], _ expression2: [T], value: (T) -> Double, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual(expression1.map(\.startDate), expression2.map(\.startDate), message(), file: file, line: line) + XCTAssertEqual(expression1.map(\.endDate), expression2.map(\.endDate), message(), file: file, line: line) + XCTAssertEqual(expression1.count, expression2.count, message(), file: file, line: line) + + for (effect, mirroredEffect) in zip(expression1, expression2) { + XCTAssertEqual(value(effect), value(mirroredEffect), accuracy: 0.0000000001, message(), file: file, line: line) + } +} + +func XCTAssertSampleValueEqual(_ expression1: [GlucoseEffect], _ expression2: [GlucoseEffect], _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + XCTAssertSampleValueEqual(expression1, expression2, value: \.mgdl, message(), file: file, line: line) +} + +func XCTAssertSampleValueEqual(_ expression1: [GlucoseChange], _ expression2: [GlucoseChange], _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + XCTAssertSampleValueEqual(expression1, expression2, value: \.mgdl, message(), file: file, line: line) +} diff --git a/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift b/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift index fc76742..1c55db7 100644 --- a/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift +++ b/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift @@ -29,10 +29,10 @@ extension AlgorithmInputFixture { doses: [], carbEntries: [], basal: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 1.0)], - sensitivity: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: forecastEnd, value: .glucose(55))], + sensitivity: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: forecastEnd, value: 55)], carbRatio: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 10)], - target: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: ClosedRange(uncheckedBounds: (lower: .glucose(100), upper: .glucose(110))))], - suspendThreshold: .glucose(65), + target: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: ClosedRange(uncheckedBounds: (lower: 100, upper: 110)))], + suspendThreshold: 65, maxBolus: 6, maxBasalRate: 8, recommendationInsulinType: .novolog, diff --git a/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift b/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift index 25f6096..2f8e381 100644 --- a/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift +++ b/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift @@ -31,7 +31,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { let start = input.glucoseHistory.last!.startDate // Standard prediction (full annotation inside generatePrediction) - let standard = LoopAlgorithm.generatePrediction( + let standard = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, doses: input.doses, @@ -45,7 +45,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { // Pre-annotate once (ISF-independent); no effects → standard inner glucoseEffects path let precomputed = PrecomputedInsulinInput.annotate(doses: input.doses, basal: input.basal) - let fast = LoopAlgorithm.generatePrediction( + let fast = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, precomputedInsulin: precomputed, @@ -79,7 +79,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { let input = try loadInput() let start = input.glucoseHistory.last!.startDate - let standard = LoopAlgorithm.generatePrediction( + let standard = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, doses: input.doses, @@ -95,7 +95,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { .annotate(doses: input.doses, basal: input.basal) .withEffects(sensitivity: input.sensitivity) - let fast = LoopAlgorithm.generatePrediction( + let fast = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, precomputedInsulin: precomputed, @@ -116,7 +116,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { let input = try loadInput() let start = input.glucoseHistory.last!.startDate - let standard = LoopAlgorithm.generatePrediction( + let standard = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, doses: input.doses, @@ -130,7 +130,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { // Simulate EvalCore: build once, then pass the (unsliced) annotated set let sliced = PrecomputedInsulinInput.annotate(doses: input.doses, basal: input.basal) - let fromSlice = LoopAlgorithm.generatePrediction( + let fromSlice = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, precomputedInsulin: sliced, @@ -162,16 +162,8 @@ final class PrecomputedInsulinInputTests: XCTestCase { for multiplier in multipliers { // Scale ISF — O(n_isf_segments), negligible. - // Preserve whatever unit the fixture uses by scaling the raw double - // and re-wrapping in the same unit. - let scaledSensitivity = input.sensitivity.map { entry -> AbsoluteScheduleValue in - let unit = entry.value.unit - let scaled = entry.value.doubleValue(for: unit) * multiplier - return AbsoluteScheduleValue( - startDate: entry.startDate, - endDate: entry.endDate, - value: LoopQuantity(unit: unit, doubleValue: scaled) - ) + let scaledSensitivity = input.sensitivity.map { entry in + AbsoluteScheduleValue(startDate: entry.startDate, endDate: entry.endDate, value: entry.value * multiplier) } // Compute effects once for this ISF value — O(D × T), not per-step @@ -179,7 +171,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { XCTAssertNotNil(precomputed.insulinEffects, "withEffects should populate insulinEffects") // Verify it produces the same result as the standard path with the same scaled ISF - let standard = LoopAlgorithm.generatePrediction( + let standard = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, doses: input.doses, @@ -190,7 +182,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) - let fast = LoopAlgorithm.generatePrediction( + let fast = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, precomputedInsulin: precomputed, From df061c4963d3eb6c94c323d53b629f4f526b133b Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 1 Nov 2026 00:22:46 -0500 Subject: [PATCH 2/3] Name the plain-number inputs by unit and convert them once - AlgorithmInput's sensitivity, target and suspendThreshold become sensitivityMgdlPerUnit, targetMgdl and suspendThresholdMgdl, and the public overloads that take plain numbers label them the same way (momentumVelocityMaximumMgdlPerMinute, insulinSensitivityHistoryMgdlPerUnit, ...). A value in the wrong unit no longer passes unnoticed at the call site. Fixture JSON keys are unchanged. - run converts the schedules to quantities once instead of on each access. - Remove FixtureGlucoseSample.condition and the now unused GlucoseCondition. --- Sources/LoopAlgorithm/AlgorithmInput.swift | 12 ++-- .../LoopAlgorithm/AlgorithmInputFixture.swift | 40 ++++++------ Sources/LoopAlgorithm/Carbs/CarbMath.swift | 8 +-- .../Glucose/FixtureGlucoseSample.swift | 1 - .../Glucose/GlucoseCondition.swift | 12 ---- .../LoopAlgorithm/Insulin/InsulinMath.swift | 12 ++-- .../Insulin/PrecomputedInsulinInput.swift | 27 ++++---- Sources/LoopAlgorithm/LoopAlgorithm.swift | 61 ++++++++++--------- .../LoopAlgorithm/LoopPredictionInput.swift | 32 +++++----- .../AlgorithmExperimentsTests.swift | 2 +- .../LoopAlgorithmTests/GlucoseMathTests.swift | 8 +-- .../LoopAlgorithmTests.swift | 22 +++---- .../Mocks/LoopAlgorithmInputMock.swift | 6 +- .../PrecomputedInsulinInputTests.swift | 22 +++---- 14 files changed, 125 insertions(+), 140 deletions(-) delete mode 100644 Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift diff --git a/Sources/LoopAlgorithm/AlgorithmInput.swift b/Sources/LoopAlgorithm/AlgorithmInput.swift index dcf793d..77f5d69 100644 --- a/Sources/LoopAlgorithm/AlgorithmInput.swift +++ b/Sources/LoopAlgorithm/AlgorithmInput.swift @@ -20,11 +20,11 @@ public protocol AlgorithmInput { var carbEntries: [CarbType] { get } var basal: [AbsoluteScheduleValue] { get } /// Insulin sensitivity in mg/dL per unit - var sensitivity: [AbsoluteScheduleValue] { get } + var sensitivityMgdlPerUnit: [AbsoluteScheduleValue] { get } var carbRatio: [AbsoluteScheduleValue] { get } - var target: GlucoseRangeTimeline { get } + var targetMgdl: GlucoseRangeTimeline { get } /// Suspend threshold in mg/dL - var suspendThreshold: Double? { get } + var suspendThresholdMgdl: Double? { get } var maxBolus: Double { get } var maxActiveInsulinMultiplier: Double? { get } // Defaults to 2 (2x maxBolus) var maxBasalRate: Double { get } @@ -54,7 +54,7 @@ public extension AlgorithmInput { } extension AlgorithmInput { - var sensitivityQuantities: [AbsoluteScheduleValue] { sensitivity.mgdlQuantities } - var targetQuantities: QuantityRangeTimeline { target.quantities } - var suspendThresholdQuantity: LoopQuantity? { suspendThreshold.map(LoopQuantity.mgdl) } + var sensitivityQuantities: [AbsoluteScheduleValue] { sensitivityMgdlPerUnit.mgdlQuantities } + var targetQuantities: QuantityRangeTimeline { targetMgdl.quantities } + var suspendThresholdQuantity: LoopQuantity? { suspendThresholdMgdl.map(LoopQuantity.mgdl) } } diff --git a/Sources/LoopAlgorithm/AlgorithmInputFixture.swift b/Sources/LoopAlgorithm/AlgorithmInputFixture.swift index bcecc75..1c6ecc8 100644 --- a/Sources/LoopAlgorithm/AlgorithmInputFixture.swift +++ b/Sources/LoopAlgorithm/AlgorithmInputFixture.swift @@ -20,10 +20,10 @@ public struct AlgorithmInputFixture: AlgorithmInput { public var doses: [FixtureInsulinDose] public var carbEntries: [FixtureCarbEntry] public var basal: [AbsoluteScheduleValue] - public var sensitivity: [AbsoluteScheduleValue] + public var sensitivityMgdlPerUnit: [AbsoluteScheduleValue] public var carbRatio: [AbsoluteScheduleValue] - public var target: GlucoseRangeTimeline - public var suspendThreshold: Double? + public var targetMgdl: GlucoseRangeTimeline + public var suspendThresholdMgdl: Double? public var maxBolus: Double public var maxActiveInsulinMultiplier: Double? public var maxBasalRate: Double @@ -63,10 +63,10 @@ public struct AlgorithmInputFixture: AlgorithmInput { doses: [FixtureInsulinDose], carbEntries: [FixtureCarbEntry], basal: [AbsoluteScheduleValue], - sensitivity: [AbsoluteScheduleValue], + sensitivityMgdlPerUnit: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], - target: GlucoseRangeTimeline, - suspendThreshold: Double?, + targetMgdl: GlucoseRangeTimeline, + suspendThresholdMgdl: Double?, maxBolus: Double, maxActiveInsulinMultiplier: Double? = nil, maxBasalRate: Double, @@ -85,10 +85,10 @@ public struct AlgorithmInputFixture: AlgorithmInput { self.doses = doses self.carbEntries = carbEntries self.basal = basal - self.sensitivity = sensitivity + self.sensitivityMgdlPerUnit = sensitivityMgdlPerUnit self.carbRatio = carbRatio - self.target = target - self.suspendThreshold = suspendThreshold + self.targetMgdl = targetMgdl + self.suspendThresholdMgdl = suspendThresholdMgdl self.maxBolus = maxBolus self.maxActiveInsulinMultiplier = maxActiveInsulinMultiplier self.maxBasalRate = maxBasalRate @@ -114,14 +114,14 @@ extension AlgorithmInputFixture: Codable { self.doses = try container.decode([FixtureInsulinDose].self, forKey: .doses) self.carbEntries = try container.decode([FixtureCarbEntry].self, forKey: .carbEntries) self.basal = try container.decode([AbsoluteScheduleValue].self, forKey: .basal) - self.sensitivity = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) + self.sensitivityMgdlPerUnit = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) self.carbRatio = try container.decode([AbsoluteScheduleValue].self, forKey: .carbRatio) - let targetMgdl = try container.decode([TargetEntry].self, forKey: .target) - self.target = targetMgdl.map { + let targetEntries = try container.decode([TargetEntry].self, forKey: .target) + self.targetMgdl = targetEntries.map { let range = ClosedRange(uncheckedBounds: (lower: $0.lowerBound, upper: $0.upperBound)) return AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: range) } - self.suspendThreshold = try container.decodeIfPresent(Double.self, forKey: .suspendThreshold) + self.suspendThresholdMgdl = try container.decodeIfPresent(Double.self, forKey: .suspendThreshold) self.maxBolus = try container.decode(Double.self, forKey: .maxBolus) self.maxActiveInsulinMultiplier = try container.decodeIfPresent(Double.self, forKey: .maxActiveInsulinMultiplier) self.maxBasalRate = try container.decode(Double.self, forKey: .maxBasalRate) @@ -163,13 +163,13 @@ extension AlgorithmInputFixture: Codable { try container.encode(doses, forKey: .doses) try container.encode(carbEntries, forKey: .carbEntries) try container.encode(basal, forKey: .basal) - try container.encode(sensitivity, forKey: .sensitivity) + try container.encode(sensitivityMgdlPerUnit, forKey: .sensitivity) try container.encode(carbRatio, forKey: .carbRatio) - let targetMgdl = target.map { + let targetEntries = targetMgdl.map { TargetEntry(startDate: $0.startDate, endDate: $0.endDate, lowerBound: $0.value.lowerBound, upperBound: $0.value.upperBound) } - try container.encode(targetMgdl, forKey: .target) - try container.encode(suspendThreshold, forKey: .suspendThreshold) + try container.encode(targetEntries, forKey: .target) + try container.encode(suspendThresholdMgdl, forKey: .suspendThreshold) try container.encode(maxBolus, forKey: .maxBolus) try container.encode(maxActiveInsulinMultiplier, forKey: .maxActiveInsulinMultiplier) try container.encode(maxBasalRate, forKey: .maxBasalRate) @@ -227,10 +227,10 @@ extension AlgorithmInputFixture { doses: input.doses.map(\.asFixtureInsulinDose), carbEntries: input.carbEntries.map(\.asFixtureCarbEntry), basal: input.basal, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, - target: input.target, - suspendThreshold: input.suspendThreshold, + targetMgdl: input.targetMgdl, + suspendThresholdMgdl: input.suspendThresholdMgdl, maxBolus: input.maxBolus, maxBasalRate: input.maxBasalRate, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection, diff --git a/Sources/LoopAlgorithm/Carbs/CarbMath.swift b/Sources/LoopAlgorithm/Carbs/CarbMath.swift index 30d891f..8776509 100644 --- a/Sources/LoopAlgorithm/Carbs/CarbMath.swift +++ b/Sources/LoopAlgorithm/Carbs/CarbMath.swift @@ -797,13 +797,13 @@ public extension Collection { from start: Date? = nil, to end: Date? = nil, carbRatios: [AbsoluteScheduleValue], - insulinSensitivities: [AbsoluteScheduleValue], + insulinSensitivitiesMgdlPerUnit: [AbsoluteScheduleValue], defaultAbsorptionTime: TimeInterval = CarbMath.defaultAbsorptionTime, absorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), delay: TimeInterval = CarbMath.defaultEffectDelay, delta: TimeInterval = GlucoseMath.defaultDelta ) -> [GlucoseEffect] where Element == CarbStatus { - dynamicGlucoseEffects(from: start, to: end, carbRatios: carbRatios, insulinSensitivities: insulinSensitivities.mgdlQuantities, + dynamicGlucoseEffects(from: start, to: end, carbRatios: carbRatios, insulinSensitivities: insulinSensitivitiesMgdlPerUnit.mgdlQuantities, defaultAbsorptionTime: defaultAbsorptionTime, absorptionModel: absorptionModel, delay: delay, delta: delta) } } @@ -813,7 +813,7 @@ public extension Collection where Element: CarbEntry { func map( to effectVelocities: [GlucoseEffectVelocity], carbRatio: [AbsoluteScheduleValue], - insulinSensitivity: [AbsoluteScheduleValue], + insulinSensitivityMgdlPerUnit: [AbsoluteScheduleValue], absorptionTimeOverrun: Double = CarbMath.defaultAbsorptionTimeOverrun, defaultAbsorptionTime: TimeInterval = CarbMath.defaultAbsorptionTime, delay: TimeInterval = CarbMath.defaultEffectDelay, @@ -822,7 +822,7 @@ public extension Collection where Element: CarbEntry { adaptiveAbsorptionRateEnabled: Bool = false, adaptiveRateStandbyIntervalFraction: Double = 0.2 ) -> [CarbStatus] { - map(to: effectVelocities, carbRatio: carbRatio, insulinSensitivity: insulinSensitivity.mgdlQuantities, + map(to: effectVelocities, carbRatio: carbRatio, insulinSensitivity: insulinSensitivityMgdlPerUnit.mgdlQuantities, absorptionTimeOverrun: absorptionTimeOverrun, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, initialAbsorptionTimeOverrun: initialAbsorptionTimeOverrun, absorptionModel: absorptionModel, adaptiveAbsorptionRateEnabled: adaptiveAbsorptionRateEnabled, adaptiveRateStandbyIntervalFraction: adaptiveRateStandbyIntervalFraction) diff --git a/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift b/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift index 6870e43..7af18f3 100644 --- a/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift +++ b/Sources/LoopAlgorithm/Glucose/FixtureGlucoseSample.swift @@ -15,7 +15,6 @@ public struct FixtureGlucoseSample: GlucoseSampleValue, Equatable { public let mgdl: Double public let isDisplayOnly: Bool public let wasUserEntered: Bool - var condition: GlucoseCondition? public init( provenanceIdentifier: String = Self.defaultProvenanceIdentifier, diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift b/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift deleted file mode 100644 index 5b2de61..0000000 --- a/Sources/LoopAlgorithm/Glucose/GlucoseCondition.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// GlucoseCondition.swift -// LoopAlgorithm -// -// Created by Darin Krauss on 9/3/21. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -enum GlucoseCondition: String, Codable { - case belowRange - case aboveRange -} diff --git a/Sources/LoopAlgorithm/Insulin/InsulinMath.swift b/Sources/LoopAlgorithm/Insulin/InsulinMath.swift index eb23af9..a162d0f 100644 --- a/Sources/LoopAlgorithm/Insulin/InsulinMath.swift +++ b/Sources/LoopAlgorithm/Insulin/InsulinMath.swift @@ -547,34 +547,34 @@ extension Collection where Element == BasalRelativeDose { public extension Collection where Element == BasalRelativeDose { /// Glucose effects of the doses. Sensitivity is in mg/dL per unit. func glucoseEffects( - insulinSensitivityHistory: [AbsoluteScheduleValue], + insulinSensitivityHistoryMgdlPerUnit: [AbsoluteScheduleValue], from start: Date? = nil, to end: Date? = nil, delta: TimeInterval = TimeInterval(/* minutes: */60 * 5) ) -> [GlucoseEffect] { - glucoseEffects(insulinSensitivityHistory: insulinSensitivityHistory.mgdlQuantities, from: start, to: end, delta: delta) + glucoseEffects(insulinSensitivityHistory: insulinSensitivityHistoryMgdlPerUnit.mgdlQuantities, from: start, to: end, delta: delta) } /// Glucose effects of the doses using mid-absorption ISF. Sensitivity is in mg/dL per unit. func glucoseEffectsMidAbsorptionISF( longestEffectDuration: TimeInterval = InsulinMath.defaultInsulinActivityDuration, - insulinSensitivityHistory: [AbsoluteScheduleValue], + insulinSensitivityHistoryMgdlPerUnit: [AbsoluteScheduleValue], from start: Date? = nil, to end: Date? = nil, delta: TimeInterval = TimeInterval(/* minutes: */60 * 5) ) -> [GlucoseEffect] { - glucoseEffectsMidAbsorptionISF(longestEffectDuration: longestEffectDuration, insulinSensitivityHistory: insulinSensitivityHistory.mgdlQuantities, + glucoseEffectsMidAbsorptionISF(longestEffectDuration: longestEffectDuration, insulinSensitivityHistory: insulinSensitivityHistoryMgdlPerUnit.mgdlQuantities, from: start, to: end, delta: delta) } /// Glucose effects of the doses at the given dates. Sensitivity is in mg/dL per unit. func glucoseEffects( longestEffectDuration: TimeInterval = InsulinMath.defaultInsulinActivityDuration, - insulinSensitivityTimeline: [AbsoluteScheduleValue], + insulinSensitivityTimelineMgdlPerUnit: [AbsoluteScheduleValue], effectDates: [Date], delta: TimeInterval = TimeInterval(/* minutes: */60 * 5) ) -> [GlucoseEffect] { - glucoseEffects(longestEffectDuration: longestEffectDuration, insulinSensitivityTimeline: insulinSensitivityTimeline.mgdlQuantities, + glucoseEffects(longestEffectDuration: longestEffectDuration, insulinSensitivityTimeline: insulinSensitivityTimelineMgdlPerUnit.mgdlQuantities, effectDates: effectDates, delta: delta) } } diff --git a/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift b/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift index ce58dc8..1c83ad1 100644 --- a/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift +++ b/Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift @@ -20,7 +20,8 @@ // │ let base = PrecomputedInsulinInput.annotate(doses: doses, basal: basal)│ // │ for multiplier in [0.7, 0.8, 0.9, 1.0, 1.1, ...] { │ // │ let scaled = scaledSensitivity(sensitivity, by: multiplier) │ -// │ let input = base.withEffects(sensitivity: scaled) // O(D×T) once │ +// │ let input = base.withEffects(sensitivityMgdlPerUnit: scaled) │ +// │ // O(D×T) once │ // │ for t in sweepSteps { │ // │ let result = Algorithm.generatePrediction( │ // │ start: t, ..., precomputedInsulin: input, ...) // no annotation │ @@ -58,8 +59,8 @@ private extension Array { /// /// // 2. For each ISF value: compute effects once, sweep all time steps /// for multiplier in isfMultipliers { -/// let input = base.withEffects(sensitivity: scale(sensitivity, by: multiplier), -/// from: sweepStart, to: sweepEnd + activityDuration) +/// let input = base.withEffects(sensitivityMgdlPerUnit: scale(sensitivity, by: multiplier), +/// from: sweepStart, to: sweepEnd + activityDuration) /// for t in sweepSteps { /// let prediction = Algorithm.generatePrediction( /// start: t, glucoseHistory: cgm[t], precomputedInsulin: input, ...) @@ -86,13 +87,13 @@ public struct PrecomputedInsulinInput { /// When non-nil, `generatePrediction` uses this directly instead of /// calling `glucoseEffects(insulinSensitivityHistory:from:to:)`. /// - /// **ISF sweeps:** rebuild this once per multiplier using `withEffects(sensitivity:)`. + /// **ISF sweeps:** rebuild this once per multiplier using `withEffects(sensitivityMgdlPerUnit:)`. /// The `annotatedDoses` array is unchanged and does not need to be rebuilt. /// /// **Timeline coverage:** must cover /// `[glucoseHistory.first.startDate, sweepEnd + defaultInsulinActivityDuration]` /// for all steps in the sweep. Pass a generous `to:` date when calling - /// `withEffects(sensitivity:from:to:)`. + /// `withEffects(sensitivityMgdlPerUnit:from:to:)`. public var insulinEffects: [GlucoseEffect]? // MARK: - Init @@ -117,7 +118,7 @@ extension PrecomputedInsulinInput { /// - doses: All insulin doses for the sweep window, sorted by startDate. /// - basal: Scheduled basal timeline covering the same window. /// - Returns: A `PrecomputedInsulinInput` with `insulinEffects == nil`. - /// Call `withEffects(sensitivity:from:to:)` before passing to + /// Call `withEffects(sensitivityMgdlPerUnit:from:to:)` before passing to /// `generatePrediction`. public static func annotate( doses: [DoseType], @@ -133,7 +134,7 @@ extension PrecomputedInsulinInput { /// pass the result into every `generatePrediction` call for that multiplier. /// /// - Parameters: - /// - sensitivity: The (possibly scaled) ISF timeline for this sweep config. + /// - sensitivityMgdlPerUnit: The (possibly scaled) ISF timeline for this sweep config, in mg/dL per unit. /// - from: Start of the effect timeline. Defaults to earliest dose start. /// Should be <= `glucoseHistory.first.startDate` for the first eval step. /// - to: End of the effect timeline. Should cover @@ -142,12 +143,12 @@ extension PrecomputedInsulinInput { /// - useMidAbsorptionISF: Use mid-absorption ISF computation. /// - Returns: A new `PrecomputedInsulinInput` with `insulinEffects` populated. public func withEffects( - sensitivity: [AbsoluteScheduleValue], + sensitivityMgdlPerUnit: [AbsoluteScheduleValue], from: Date? = nil, to: Date? = nil, useMidAbsorptionISF: Bool = false ) -> PrecomputedInsulinInput { - let sensitivity = sensitivity.mgdlQuantities + let sensitivity = sensitivityMgdlPerUnit.mgdlQuantities let effects: [GlucoseEffect] if useMidAbsorptionISF { effects = annotatedDoses.glucoseEffectsMidAbsorptionISF( @@ -194,20 +195,20 @@ extension PrecomputedInsulinInput { /// Convenience: annotate and compute effects in one call. /// /// Use when running a single config (no ISF sweep). For ISF sweeps, - /// prefer `annotate(doses:basal:)` + `withEffects(sensitivity:from:to:)` + /// prefer `annotate(doses:basal:)` + `withEffects(sensitivityMgdlPerUnit:from:to:)` /// so annotation cost is paid only once. public static func build( doses: [DoseType], basal: [AbsoluteScheduleValue], - sensitivity: [AbsoluteScheduleValue]? = nil, + sensitivityMgdlPerUnit: [AbsoluteScheduleValue]? = nil, effectsFrom: Date? = nil, effectsTo: Date? = nil, useMidAbsorptionISF: Bool = false ) -> PrecomputedInsulinInput { let base = annotate(doses: doses, basal: basal) - guard let sensitivity else { return base } + guard let sensitivityMgdlPerUnit else { return base } return base.withEffects( - sensitivity: sensitivity, + sensitivityMgdlPerUnit: sensitivityMgdlPerUnit, from: effectsFrom, to: effectsTo, useMidAbsorptionISF: useMidAbsorptionISF diff --git a/Sources/LoopAlgorithm/LoopAlgorithm.swift b/Sources/LoopAlgorithm/LoopAlgorithm.swift index eeca9d0..b132e54 100644 --- a/Sources/LoopAlgorithm/LoopAlgorithm.swift +++ b/Sources/LoopAlgorithm/LoopAlgorithm.swift @@ -151,34 +151,33 @@ public struct Algorithm { /// - doses: History of insulin doses: t-16h to t /// - carbEntries: History of carb entries: t-10h to t /// - basal: Scheduled basal rate timeline: t-16h to t - /// - sensitivity: Insulin sensitivity timeline: t-16h to t (eventually with mid-absorption isf changes, it will be t-10h to t) + /// - sensitivityMgdlPerUnit: Insulin sensitivity timeline in mg/dL per unit: t-16h to t (eventually with mid-absorption isf changes, it will be t-10h to t) /// - carbRatio: Carb ratio timeline: t-10h to t+6h - /// - target: Correction range timeline. Used only for the integral-correction clamp when `emulation.integralRCClamp` is enabled; may be empty otherwise. + /// - targetMgdl: Correction range timeline in mg/dL. Used only for the integral-correction clamp when `emulation.integralRCClamp` is enabled; may be empty otherwise. /// - algorithmEffectsOptions: Which effects to include when combining effects to generate glucose prediction /// - useIntegralRetrospectiveCorrection: If true, the prediction will use Integral Retrospection. If false, will use traditional Retrospective Correction /// - includingPositiveVelocityAndRC: If false, only net negative momentum and RC effects will used. /// - carbAbsorptionModel: A model conforming to CarbAbsorptionComputable that is used for computing carb absorption over time. /// - emulation: Options for reproducing earlier deployed algorithm behavior; the default emulates nothing. - /// - momentumVelocityMaximum: Limit on the momentum effect, in mg/dL per minute + /// - momentumVelocityMaximumMgdlPerMinute: Limit on the momentum effect, in mg/dL per minute /// - Returns: A LoopPrediction struct containing the predicted glucose and the computed intermediate effects used to make the prediction /// - /// Sensitivity is in mg/dL per unit and target is in mg/dL. public static func generatePrediction( start: Date, glucoseHistory: [GlucoseType], doses: [InsulinDoseType], carbEntries: [CarbType], basal: [AbsoluteScheduleValue], - sensitivity: [AbsoluteScheduleValue], + sensitivityMgdlPerUnit: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], - target: GlucoseRangeTimeline = [], + targetMgdl: GlucoseRangeTimeline = [], algorithmEffectsOptions: AlgorithmEffectsOptions = .all, useIntegralRetrospectiveCorrection: Bool = false, includingPositiveVelocityAndRC: Bool = true, useMidAbsorptionISF: Bool = false, carbAbsorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), gradualTransitionsThreshold: Double? = 40.0, - momentumVelocityMaximum: Double? = nil, + momentumVelocityMaximumMgdlPerMinute: Double? = nil, emulation: AlgorithmEmulationOptions = AlgorithmEmulationOptions(), experiments: Set = [] ) -> LoopPrediction where CarbType: CarbEntry, GlucoseType: GlucoseSampleValue, InsulinDoseType: InsulinDose { @@ -188,16 +187,16 @@ public struct Algorithm { doses: doses, carbEntries: carbEntries, basal: basal, - sensitivity: sensitivity.mgdlQuantities, + sensitivity: sensitivityMgdlPerUnit.mgdlQuantities, carbRatio: carbRatio, - target: target.quantities, + target: targetMgdl.quantities, algorithmEffectsOptions: algorithmEffectsOptions, useIntegralRetrospectiveCorrection: useIntegralRetrospectiveCorrection, includingPositiveVelocityAndRC: includingPositiveVelocityAndRC, useMidAbsorptionISF: useMidAbsorptionISF, carbAbsorptionModel: carbAbsorptionModel, gradualTransitionsThreshold: gradualTransitionsThreshold, - momentumVelocityMaximum: momentumVelocityMaximum.map { LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) }, + momentumVelocityMaximum: momentumVelocityMaximumMgdlPerMinute.map { LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) }, emulation: emulation, experiments: experiments ) @@ -446,7 +445,7 @@ public struct Algorithm { /// must slice `annotatedDoses` to `[t - insulinLookback, t]` (or /// `[t - lookback, t + 6h]` for future-insulin mode). /// - carbEntries: History of carb entries. - /// - sensitivity: ISF timeline — still required for carb + RC effects. + /// - sensitivityMgdlPerUnit: ISF timeline in mg/dL per unit — still required for carb + RC effects. /// - carbRatio: Carb ratio timeline. /// - algorithmEffectsOptions: Which effects to include. /// - useIntegralRetrospectiveCorrection: Use integral RC. @@ -462,9 +461,9 @@ public struct Algorithm { glucoseHistory: [GlucoseType], precomputedInsulin: PrecomputedInsulinInput, carbEntries: [CarbType], - sensitivity: [AbsoluteScheduleValue], + sensitivityMgdlPerUnit: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], - target: GlucoseRangeTimeline = [], + targetMgdl: GlucoseRangeTimeline = [], scheduledBasalRate: Double? = nil, algorithmEffectsOptions: AlgorithmEffectsOptions = .all, useIntegralRetrospectiveCorrection: Bool = false, @@ -472,7 +471,7 @@ public struct Algorithm { useMidAbsorptionISF: Bool = false, carbAbsorptionModel: CarbAbsorptionComputable = PiecewiseLinearAbsorption(), gradualTransitionsThreshold: Double? = 40.0, - momentumVelocityMaximum: Double? = nil, + momentumVelocityMaximumMgdlPerMinute: Double? = nil, emulation: AlgorithmEmulationOptions = AlgorithmEmulationOptions() ) -> LoopPrediction where CarbType: CarbEntry, GlucoseType: GlucoseSampleValue { generatePrediction( @@ -480,9 +479,9 @@ public struct Algorithm { glucoseHistory: glucoseHistory, precomputedInsulin: precomputedInsulin, carbEntries: carbEntries, - sensitivity: sensitivity.mgdlQuantities, + sensitivity: sensitivityMgdlPerUnit.mgdlQuantities, carbRatio: carbRatio, - target: target.quantities, + target: targetMgdl.quantities, scheduledBasalRate: scheduledBasalRate, algorithmEffectsOptions: algorithmEffectsOptions, useIntegralRetrospectiveCorrection: useIntegralRetrospectiveCorrection, @@ -490,7 +489,7 @@ public struct Algorithm { useMidAbsorptionISF: useMidAbsorptionISF, carbAbsorptionModel: carbAbsorptionModel, gradualTransitionsThreshold: gradualTransitionsThreshold, - momentumVelocityMaximum: momentumVelocityMaximum.map { LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) }, + momentumVelocityMaximum: momentumVelocityMaximumMgdlPerMinute.map { LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) }, emulation: emulation ) } @@ -809,6 +808,10 @@ public struct Algorithm { public static func run(input: LoopAlgorithmInputType) -> AlgorithmOutput { + let scheduledSensitivity = input.sensitivityQuantities + let correctionTargets = input.targetQuantities + let suspendThresholdSetting = input.suspendThresholdQuantity + var prediction = LoopPrediction( glucose: [], effects: LoopAlgorithmEffects( @@ -857,10 +860,10 @@ public struct Algorithm { glucoseHistoryStart: glucoseStart, recommendationEffectInterval: recommendationEffectInterval ) - guard let sensitivityStartDate = input.sensitivityQuantities.first?.startDate, sensitivityStartDate <= neededISFInterval.start else { + guard let sensitivityStartDate = scheduledSensitivity.first?.startDate, sensitivityStartDate <= neededISFInterval.start else { throw AlgorithmError.sensitivityTimelineStartsTooLate } - guard let sensitivityEndDate = input.sensitivityQuantities.last?.endDate, sensitivityEndDate >= neededISFInterval.end else { + guard let sensitivityEndDate = scheduledSensitivity.last?.endDate, sensitivityEndDate >= neededISFInterval.end else { throw AlgorithmError.sensitivityTimelineEndsTooEarly } @@ -868,7 +871,7 @@ public struct Algorithm { if let oldestCarbEntry = input.carbEntries.map({ $0.startDate }).min() { guard let carbRatioStart = input.carbRatio.first?.startDate, carbRatioStart <= oldestCarbEntry, - let sensitivityStart = input.sensitivity.first?.startDate, sensitivityStart <= oldestCarbEntry + let sensitivityStart = input.sensitivityMgdlPerUnit.first?.startDate, sensitivityStart <= oldestCarbEntry else { throw AlgorithmError.carbRatioOrSensitivityStartsAfterCarbEntry } @@ -879,7 +882,7 @@ public struct Algorithm { throw AlgorithmError.basalTimelineIncomplete } - guard let suspendThreshold = input.suspendThresholdQuantity ?? input.targetQuantities.closestPrior(to: input.predictionStart)?.value.lowerBound else { + guard let suspendThreshold = suspendThresholdSetting ?? correctionTargets.closestPrior(to: input.predictionStart)?.value.lowerBound else { throw AlgorithmError.missingSuspendThreshold } @@ -892,7 +895,7 @@ public struct Algorithm { basal: input.basal, sensitivity: sensitivity, carbRatio: input.carbRatio, - target: input.targetQuantities, + target: correctionTargets, algorithmEffectsOptions: .all, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection, includingPositiveVelocityAndRC: input.includePositiveVelocityAndRC, @@ -903,7 +906,7 @@ public struct Algorithm { experiments: input.experiments ) } - prediction = predict(sensitivity: input.sensitivityQuantities) + prediction = predict(sensitivity: scheduledSensitivity) // Experiment adjustments before dosing (no-ops when `experiments` is empty). let experimentParameters = ExperimentParameters( @@ -913,13 +916,13 @@ public struct Algorithm { // Slow retrospective correction: the level comes from the forecast at the scheduled ISF; the run // then uses ISF × multiplier throughout. - var sensitivity = input.sensitivityQuantities + var sensitivity = scheduledSensitivity if let slow = experimentParameters.slowRetrospectiveCorrection { let windowStart = input.predictionStart.addingTimeInterval(-slow.window) let carbEffects = prediction.effects.carbStatus.dynamicGlucoseEffects( from: windowStart.addingTimeInterval(-GlucoseMath.defaultDelta), carbRatios: input.carbRatio, - insulinSensitivities: input.sensitivityQuantities, + insulinSensitivities: scheduledSensitivity, absorptionModel: input.carbAbsorptionModel.model) let level = slowRetrospectiveCorrectionLevel( counteraction: prediction.effects.insulinCounteraction, carbEffects: carbEffects, @@ -927,7 +930,7 @@ public struct Algorithm { let multiplier = slowRetrospectiveCorrectionMultiplier(level: level, parameters: slow) slowRetrospectiveCorrection = SlowRetrospectiveCorrectionOutput(level: level, multiplier: multiplier) if multiplier != 1 { - sensitivity = sensitivityScaled(input.sensitivityQuantities, by: multiplier) + sensitivity = sensitivityScaled(scheduledSensitivity, by: multiplier) prediction = predict(sensitivity: sensitivity) } } @@ -937,7 +940,7 @@ public struct Algorithm { var runwayShift = 0.0 if let reserve = experimentParameters.runwayReserve, let volatility, let sensitivity = sensitivity.closestPrior(to: input.predictionStart)?.value, - let target = input.targetQuantities.closestPrior(to: input.predictionStart)?.value + let target = correctionTargets.closestPrior(to: input.predictionStart)?.value { let unit = LoopUnit.milligramsPerDeciliter runwayShift = runwayAimShift( @@ -985,7 +988,7 @@ public struct Algorithm { // The runway shift lowers the aim of automatic dosing only; the manual bolus recommendation // keeps the user's target. let correctionTarget = runwayShift > 0 && input.recommendationType != .manualBolus - ? targetLowered(input.targetQuantities, by: runwayShift) : input.targetQuantities + ? targetLowered(correctionTargets, by: runwayShift) : correctionTargets var correction = insulinCorrection( prediction: prediction.glucose, at: input.predictionStart, @@ -1011,7 +1014,7 @@ public struct Algorithm { for: correction, maxBolus: input.maxBolus, currentGlucose: latestGlucose, - target: input.targetQuantities) + target: correctionTargets) result = .success(.init(manual: recommendation)) case .automaticBolus: let recommendation = recommendAutomaticDose( diff --git a/Sources/LoopAlgorithm/LoopPredictionInput.swift b/Sources/LoopAlgorithm/LoopPredictionInput.swift index f108899..69414f6 100644 --- a/Sources/LoopAlgorithm/LoopPredictionInput.swift +++ b/Sources/LoopAlgorithm/LoopPredictionInput.swift @@ -23,14 +23,14 @@ public struct LoopPredictionInput] + public var sensitivityMgdlPerUnit: [AbsoluteScheduleValue] // Expected time range coverage: t-10h to t+6h public var carbRatio: [AbsoluteScheduleValue] // Correction-range timeline. Used only for the IntegralRC integral-correction // clamp when `emulation.integralRCClamp` is enabled; may be empty otherwise. - public var target: GlucoseRangeTimeline = [] + public var targetMgdl: GlucoseRangeTimeline = [] public var algorithmEffectsOptions: AlgorithmEffectsOptions @@ -49,9 +49,9 @@ public struct LoopPredictionInput], - sensitivity: [AbsoluteScheduleValue], + sensitivityMgdlPerUnit: [AbsoluteScheduleValue], carbRatio: [AbsoluteScheduleValue], - target: GlucoseRangeTimeline = [], + targetMgdl: GlucoseRangeTimeline = [], algorithmEffectsOptions: AlgorithmEffectsOptions, useIntegralRetrospectiveCorrection: Bool, includePositiveVelocityAndRC: Bool, @@ -64,9 +64,9 @@ public struct LoopPredictionInput] { sensitivity.mgdlQuantities } - var targetQuantities: QuantityRangeTimeline { target.quantities } + var sensitivityQuantities: [AbsoluteScheduleValue] { sensitivityMgdlPerUnit.mgdlQuantities } + var targetQuantities: QuantityRangeTimeline { targetMgdl.quantities } } extension LoopPredictionInput: Codable where CarbType == FixtureCarbEntry, GlucoseType == FixtureGlucoseSample, InsulinDoseType == FixtureInsulinDose { @@ -90,14 +90,14 @@ extension LoopPredictionInput: Codable where CarbType == FixtureCarbEntry, Gluco self.doses = try container.decode([FixtureInsulinDose].self, forKey: .doses) self.carbEntries = try container.decode([FixtureCarbEntry].self, forKey: .carbEntries) self.basal = try container.decode([AbsoluteScheduleValue].self, forKey: .basal) - self.sensitivity = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) + self.sensitivityMgdlPerUnit = try container.decode([AbsoluteScheduleValue].self, forKey: .sensitivity) self.carbRatio = try container.decode([AbsoluteScheduleValue].self, forKey: .carbRatio) - if let targetMgdl = try container.decodeIfPresent([TargetEntry].self, forKey: .target) { - self.target = targetMgdl.map { + if let targetEntries = try container.decodeIfPresent([TargetEntry].self, forKey: .target) { + self.targetMgdl = targetEntries.map { AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.lowerBound...$0.upperBound) } } else { - self.target = [] + self.targetMgdl = [] } if let algorithmEffectsOptionsRaw = try container.decodeIfPresent(AlgorithmEffectsOptions.RawValue.self, forKey: .algorithmEffectsOptions) { self.algorithmEffectsOptions = AlgorithmEffectsOptions(rawValue: algorithmEffectsOptionsRaw) @@ -117,17 +117,17 @@ extension LoopPredictionInput: Codable where CarbType == FixtureCarbEntry, Gluco try container.encode(doses, forKey: .doses) try container.encode(carbEntries, forKey: .carbEntries) try container.encode(basal, forKey: .basal) - try container.encode(sensitivity, forKey: .sensitivity) + try container.encode(sensitivityMgdlPerUnit, forKey: .sensitivity) try container.encode(carbRatio, forKey: .carbRatio) - if !target.isEmpty { - let targetMgdl = target.map { + if !targetMgdl.isEmpty { + let targetEntries = targetMgdl.map { TargetEntry( startDate: $0.startDate, endDate: $0.endDate, lowerBound: $0.value.lowerBound, upperBound: $0.value.upperBound) } - try container.encode(targetMgdl, forKey: .target) + try container.encode(targetEntries, forKey: .target) } if algorithmEffectsOptions != .all { try container.encode(algorithmEffectsOptions.rawValue, forKey: .algorithmEffectsOptions) diff --git a/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift b/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift index b2b084e..4b22c82 100644 --- a/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift +++ b/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift @@ -335,7 +335,7 @@ final class AlgorithmExperimentsTests: XCTestCase { input.glucoseHistory = glucoseSeries(values, endingAt: now.addingTimeInterval(.minutes(-1))) input.doses = [] input.carbEntries = [] - input.sensitivity = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 50)] + input.sensitivityMgdlPerUnit = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 50)] input.basal = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 1.0)] input.carbRatio = [AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-96)), endDate: now.addingTimeInterval(.hours(12)), value: 10)] let standard = Algorithm.run(input: input) diff --git a/Tests/LoopAlgorithmTests/GlucoseMathTests.swift b/Tests/LoopAlgorithmTests/GlucoseMathTests.swift index de9d869..775ca7a 100644 --- a/Tests/LoopAlgorithmTests/GlucoseMathTests.swift +++ b/Tests/LoopAlgorithmTests/GlucoseMathTests.swift @@ -22,17 +22,15 @@ public struct GlucoseFixtureValue: GlucoseSampleValue { public let isDisplayOnly: Bool public let wasUserEntered: Bool public let provenanceIdentifier: String - public let condition: GlucoseCondition? let trendRate: LoopQuantity? public var syncIdentifier: String? - init(startDate: Date, quantity: LoopQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, provenanceIdentifier: String?, condition: GlucoseCondition?, trendRate: LoopQuantity?) { + init(startDate: Date, quantity: LoopQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, provenanceIdentifier: String?, trendRate: LoopQuantity?) { self.startDate = startDate self.quantity = quantity self.isDisplayOnly = isDisplayOnly self.wasUserEntered = wasUserEntered self.provenanceIdentifier = provenanceIdentifier ?? "com.loopkit.LoopKitTests" - self.condition = condition self.trendRate = trendRate } } @@ -54,7 +52,6 @@ final class GlucoseMathTests: XCTestCase { var provenanceIdentifier: String var isDisplayOnly: Bool var wasUserEntered: Bool - var condition: GlucoseCondition? var trendRate: LoopQuantity? } @@ -68,7 +65,6 @@ final class GlucoseMathTests: XCTestCase { provenanceIdentifier: provenance, isDisplayOnly: displayOnly, wasUserEntered: false, - condition: nil, trendRate: nil ) } @@ -215,7 +211,6 @@ final class GlucoseMathTests: XCTestCase { isDisplayOnly: ($0["display_only"] as? Bool) ?? false, wasUserEntered: ($0["user_entered"] as? Bool) ?? false, provenanceIdentifier: $0["provenance_identifier"] as? String, - condition: ($0["condition"] as? String).flatMap { GlucoseCondition(rawValue: $0) }, trendRate: ($0["trend_rate"] as? Double).flatMap { LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0) } ) } @@ -431,7 +426,6 @@ final class GlucoseMathTests: XCTestCase { isDisplayOnly: false, wasUserEntered: true, provenanceIdentifier: nil, - condition: nil, trendRate: nil ) } diff --git a/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift b/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift index 54462fa..152ee7a 100644 --- a/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift +++ b/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift @@ -173,7 +173,7 @@ final class LoopAlgorithmTests: XCTestCase { doses: input.doses, carbEntries: input.carbEntries, basal: input.basal, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -291,7 +291,7 @@ final class LoopAlgorithmTests: XCTestCase { let isfEnd = now.addingTimeInterval(.hours(24)) // ISF Changing mid-aborption - input.sensitivity = [ + input.sensitivityMgdlPerUnit = [ AbsoluteScheduleValue(startDate: isfStart, endDate: isfMid, value: 50), AbsoluteScheduleValue(startDate: isfMid, endDate: isfEnd, value: 100) ] @@ -383,7 +383,7 @@ final class LoopAlgorithmTests: XCTestCase { // Sensitivity doubles in one hour let isfChangeTime = now.addingTimeInterval(.hours(1)) - input.sensitivity = [ + input.sensitivityMgdlPerUnit = [ AbsoluteScheduleValue(startDate: now.addingTimeInterval(.hours(-48)), endDate: isfChangeTime, value: 50), AbsoluteScheduleValue(startDate: isfChangeTime, endDate: now.addingTimeInterval(.hours(48)), value: 100) ] @@ -424,7 +424,7 @@ final class LoopAlgorithmTests: XCTestCase { ] // Sensitivity doesn't cover start of basal dose - input.sensitivity = [ + input.sensitivityMgdlPerUnit = [ AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), value: 50), ] @@ -435,7 +435,7 @@ final class LoopAlgorithmTests: XCTestCase { } // Sensitivity does cover start of basal dose, but ends before temp basal effects end - input.sensitivity = [ + input.sensitivityMgdlPerUnit = [ AbsoluteScheduleValue( startDate: basalStart.dateFlooredToTimeInterval(GlucoseMath.defaultDelta), endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), @@ -459,7 +459,7 @@ final class LoopAlgorithmTests: XCTestCase { glucoseHistoryStart: input.glucoseHistory.first!.startDate, recommendationEffectInterval: recommendationEffectInterval ) - input.sensitivity = [ + input.sensitivityMgdlPerUnit = [ AbsoluteScheduleValue( startDate: neededISFInterval.start, endDate: neededISFInterval.end, @@ -481,7 +481,7 @@ final class LoopAlgorithmTests: XCTestCase { var input = try! decoder.decode(AlgorithmInputFixture.self, from: try! Data(contentsOf: url)) // A carb entry older than the start of the carb ratio and ISF timelines - let timelinesStart = min(input.carbRatio.first!.startDate, input.sensitivity.first!.startDate) + let timelinesStart = min(input.carbRatio.first!.startDate, input.sensitivityMgdlPerUnit.first!.startDate) let entryDate = timelinesStart.addingTimeInterval(.minutes(-30)) input.carbEntries.insert(FixtureCarbEntry(absorptionTime: .hours(3), startDate: entryDate, quantity: .carbs(20)), at: 0) @@ -500,8 +500,8 @@ final class LoopAlgorithmTests: XCTestCase { return } - let firstSensitivity = input.sensitivity.first! - input.sensitivity.insert(AbsoluteScheduleValue(startDate: entryDate, endDate: firstSensitivity.startDate, value: firstSensitivity.value), at: 0) + let firstSensitivity = input.sensitivityMgdlPerUnit.first! + input.sensitivityMgdlPerUnit.insert(AbsoluteScheduleValue(startDate: entryDate, endDate: firstSensitivity.startDate, value: firstSensitivity.value), at: 0) output = Algorithm.run(input: input) guard case .success = output.recommendationResult else { XCTFail("Expected recommendationResult success") @@ -528,7 +528,7 @@ final class LoopAlgorithmTests: XCTestCase { // Sensitivity doesn't cover forecast - input.sensitivity = [ + input.sensitivityMgdlPerUnit = [ AbsoluteScheduleValue( startDate: basalStart.dateFlooredToTimeInterval(GlucoseMath.defaultDelta), endDate: now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta), @@ -585,7 +585,7 @@ final class LoopAlgorithmTests: XCTestCase { input.predictionStart = input.predictionStart.addingTimeInterval(30) // Add more time to insulin sensitivity timeline to account for running temp basal - input.sensitivity[0].endDate = input.sensitivity[0].endDate.addingTimeInterval(.hours(1)) + input.sensitivityMgdlPerUnit[0].endDate = input.sensitivityMgdlPerUnit[0].endDate.addingTimeInterval(.hours(1)) // Manual recommendation does not cut off doses in progress, like a running temp basal input.recommendationType = .manualBolus diff --git a/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift b/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift index 1c55db7..f3bd1eb 100644 --- a/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift +++ b/Tests/LoopAlgorithmTests/Mocks/LoopAlgorithmInputMock.swift @@ -29,10 +29,10 @@ extension AlgorithmInputFixture { doses: [], carbEntries: [], basal: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 1.0)], - sensitivity: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: forecastEnd, value: 55)], + sensitivityMgdlPerUnit: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: forecastEnd, value: 55)], carbRatio: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 10)], - target: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: ClosedRange(uncheckedBounds: (lower: 100, upper: 110)))], - suspendThreshold: 65, + targetMgdl: [AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: ClosedRange(uncheckedBounds: (lower: 100, upper: 110)))], + suspendThresholdMgdl: 65, maxBolus: 6, maxBasalRate: 8, recommendationInsulinType: .novolog, diff --git a/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift b/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift index 2f8e381..3379dbc 100644 --- a/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift +++ b/Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift @@ -37,7 +37,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { doses: input.doses, carbEntries: input.carbEntries, basal: input.basal, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -50,7 +50,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { glucoseHistory: input.glucoseHistory, precomputedInsulin: precomputed, carbEntries: input.carbEntries, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -85,7 +85,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { doses: input.doses, carbEntries: input.carbEntries, basal: input.basal, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -93,14 +93,14 @@ final class PrecomputedInsulinInputTests: XCTestCase { // ISF-sweep pattern: annotate once, compute effects per ISF value let precomputed = PrecomputedInsulinInput .annotate(doses: input.doses, basal: input.basal) - .withEffects(sensitivity: input.sensitivity) + .withEffects(sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit) let fast = Algorithm.generatePrediction( start: start, glucoseHistory: input.glucoseHistory, precomputedInsulin: precomputed, carbEntries: input.carbEntries, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -122,7 +122,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { doses: input.doses, carbEntries: input.carbEntries, basal: input.basal, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -135,7 +135,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { glucoseHistory: input.glucoseHistory, precomputedInsulin: sliced, carbEntries: input.carbEntries, - sensitivity: input.sensitivity, + sensitivityMgdlPerUnit: input.sensitivityMgdlPerUnit, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -162,12 +162,12 @@ final class PrecomputedInsulinInputTests: XCTestCase { for multiplier in multipliers { // Scale ISF — O(n_isf_segments), negligible. - let scaledSensitivity = input.sensitivity.map { entry in + let scaledSensitivity = input.sensitivityMgdlPerUnit.map { entry in AbsoluteScheduleValue(startDate: entry.startDate, endDate: entry.endDate, value: entry.value * multiplier) } // Compute effects once for this ISF value — O(D × T), not per-step - let precomputed = base.withEffects(sensitivity: scaledSensitivity) + let precomputed = base.withEffects(sensitivityMgdlPerUnit: scaledSensitivity) XCTAssertNotNil(precomputed.insulinEffects, "withEffects should populate insulinEffects") // Verify it produces the same result as the standard path with the same scaled ISF @@ -177,7 +177,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { doses: input.doses, carbEntries: input.carbEntries, basal: input.basal, - sensitivity: scaledSensitivity, + sensitivityMgdlPerUnit: scaledSensitivity, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) @@ -187,7 +187,7 @@ final class PrecomputedInsulinInputTests: XCTestCase { glucoseHistory: input.glucoseHistory, precomputedInsulin: precomputed, carbEntries: input.carbEntries, - sensitivity: scaledSensitivity, + sensitivityMgdlPerUnit: scaledSensitivity, carbRatio: input.carbRatio, useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection ) From 119cfce0973290414707a1e7f08e4ce8813dd632 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 1 Nov 2026 00:49:05 -0500 Subject: [PATCH 3/3] Document the units of the public API - Units on every AlgorithmInput property, Algorithm.run(input:) and both generatePrediction overloads, including the parameters that had none. - README gains a usage section with the units table. - Fix doc comments that no longer matched the code: generatePrediction never returns nil, annotated(with:) takes basalHistory, filterDateInterval takes an interval, useLegacyIntegration is described, and the sensitivity window note no longer refers to mid-absorption ISF as future work. --- README.md | 17 +++++++++++++ Sources/LoopAlgorithm/AlgorithmInput.swift | 17 ++++++++++--- .../LoopAlgorithm/Insulin/InsulinMath.swift | 8 ++++-- Sources/LoopAlgorithm/LoopAlgorithm.swift | 25 +++++++++++++------ .../LoopAlgorithm/LoopPredictionInput.swift | 2 +- Sources/LoopAlgorithm/SampleValue.swift | 3 +-- 6 files changed, 57 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 04aac8f..02e2944 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,23 @@ Please understand that this project: * Is highly experimental * Is not approved for therapy +## Usage + +The entry point is `Algorithm.run(input:)`, which takes any type conforming to `AlgorithmInput` and returns the +glucose forecast, the effects behind it, and a dose recommendation. The API takes plain numbers in fixed units: + +| Quantity | Unit | +|---|---| +| Glucose, targets, suspend threshold | mg/dL | +| Insulin sensitivity | mg/dL per unit | +| Carbs | grams | +| Carb ratio | grams per unit | +| Insulin | units | +| Basal rates | U/h | + +Apps that store values in other units (mmol/L) convert before calling. `Algorithm.generatePrediction` produces a +forecast without a recommendation. + ## Command Line Runner There is a command line utility that will run the algorithm with a scenario input file: diff --git a/Sources/LoopAlgorithm/AlgorithmInput.swift b/Sources/LoopAlgorithm/AlgorithmInput.swift index 77f5d69..4a88b4c 100644 --- a/Sources/LoopAlgorithm/AlgorithmInput.swift +++ b/Sources/LoopAlgorithm/AlgorithmInput.swift @@ -9,6 +9,7 @@ import Foundation +/// The inputs to `Algorithm.run(input:)`. Glucose values are in mg/dL, carbs in grams and insulin in units. public protocol AlgorithmInput { associatedtype CarbType: CarbEntry associatedtype GlucoseType: GlucoseSampleValue @@ -18,15 +19,21 @@ public protocol AlgorithmInput { var glucoseHistory: [GlucoseType] { get } var doses: [InsulinDoseType] { get } var carbEntries: [CarbType] { get } + /// Scheduled basal rate in U/h var basal: [AbsoluteScheduleValue] { get } /// Insulin sensitivity in mg/dL per unit var sensitivityMgdlPerUnit: [AbsoluteScheduleValue] { get } + /// Carb ratio in grams per unit var carbRatio: [AbsoluteScheduleValue] { get } + /// Correction range in mg/dL var targetMgdl: GlucoseRangeTimeline { get } /// Suspend threshold in mg/dL var suspendThresholdMgdl: Double? { get } + /// Maximum bolus in units var maxBolus: Double { get } - var maxActiveInsulinMultiplier: Double? { get } // Defaults to 2 (2x maxBolus) + /// Limit on active insulin, as a multiple of `maxBolus`. Defaults to 2. + var maxActiveInsulinMultiplier: Double? { get } + /// Maximum basal rate in U/h var maxBasalRate: Double { get } var useIntegralRetrospectiveCorrection: Bool { get } var includePositiveVelocityAndRC: Bool { get } @@ -34,9 +41,13 @@ public protocol AlgorithmInput { var carbAbsorptionModel: CarbAbsorptionModel { get } var recommendationInsulinModel: InsulinModel { get } var recommendationType: DoseRecommendationType { get } - var automaticBolusApplicationFactor: Double? { get } // Defaults to 0.4 + /// Fraction of the correction given as an automatic bolus. Defaults to 0.4. + var automaticBolusApplicationFactor: Double? { get } + /// Largest jump in mg/dL between consecutive glucose readings for retrospective correction to apply. + /// Defaults to 40. var gradualTransitionsThreshold: Double? { get } - var emulation: AlgorithmEmulationOptions? { get } // Defaults to nil (no emulation) + /// Options for reproducing earlier deployed algorithm behavior. Defaults to nil (no emulation). + var emulation: AlgorithmEmulationOptions? { get } /// Optional algorithm experiments; the empty set is standard LoopAlgorithm. Conforming types that do /// not provide it get the empty set. var experiments: Set { get } diff --git a/Sources/LoopAlgorithm/Insulin/InsulinMath.swift b/Sources/LoopAlgorithm/Insulin/InsulinMath.swift index a162d0f..1cd9fcf 100644 --- a/Sources/LoopAlgorithm/Insulin/InsulinMath.swift +++ b/Sources/LoopAlgorithm/Insulin/InsulinMath.swift @@ -224,7 +224,7 @@ extension Collection where Element: InsulinDose { /// /// Doses which cross time boundaries in the basal rate schedule are split into multiple entries. /// - /// - Parameter basalSchedule: A history of basal rates covering the timespan of these doses. + /// - Parameter basalHistory: A history of basal rates covering the timespan of these doses. /// - Parameter fillBasalGaps: If true, the returned array will interpolate doses from basal schedule for those parts of the /// timeline that this array does not cover. /// - Returns: An array of annotated dose entries @@ -286,7 +286,7 @@ extension Collection where Element: InsulinDose { /// /// Doses which cross time boundaries in the basal rate schedule are split into multiple entries. /// - /// - Parameter basalSchedule: A history of basal rates covering the timespan of these doses. + /// - Parameter basalHistory: A history of basal rates covering the timespan of these doses. /// - Returns: An array of annotated dose entries public func annotated(with basalHistory: [AbsoluteScheduleValue]) -> [BasalRelativeDose] { var annotatedDoses: [BasalRelativeDose] = [] @@ -314,6 +314,8 @@ extension Collection where Element == BasalRelativeDose { - parameter start: The date to start the timeline - parameter end: The date to end the timeline - parameter delta: The differential between timeline entries, Defaults to 5 minutes. + - parameter useLegacyIntegration: If true, integrate basal doses on the delta grid as deployed Loop did before + the continuous integration, which ripples by up to one delta. - returns: A sequence of insulin amount remaining */ @@ -347,6 +349,8 @@ extension Collection where Element == BasalRelativeDose { Calculates insulin remaining at a given point in time for a collection of doses - parameter date: The date at which to calculate remaining insulin. If nil, current date is used. + - parameter useLegacyIntegration: If true, integrate basal doses on the delta grid as deployed Loop did before + the continuous integration, which ripples by up to one delta. - returns: Insulin amount remaining at specified time */ diff --git a/Sources/LoopAlgorithm/LoopAlgorithm.swift b/Sources/LoopAlgorithm/LoopAlgorithm.swift index b132e54..8d84b1e 100644 --- a/Sources/LoopAlgorithm/LoopAlgorithm.swift +++ b/Sources/LoopAlgorithm/LoopAlgorithm.swift @@ -142,24 +142,24 @@ public struct Algorithm { /// Generates a forecast predicting glucose. /// Outputs may be incomplete, if there are issues with the provided data, but as many intermediate derived fields as can be computed, will be computed. /// - /// Returns nil if the normal scheduled basal, or active temporary basal, is sufficient. - /// - /// /// - Parameters: /// - start: The starting time of the glucose prediction. /// - glucoseHistory: History of glucose values: t-10h to t. Must include at least one value. /// - doses: History of insulin doses: t-16h to t /// - carbEntries: History of carb entries: t-10h to t /// - basal: Scheduled basal rate timeline: t-16h to t - /// - sensitivityMgdlPerUnit: Insulin sensitivity timeline in mg/dL per unit: t-16h to t (eventually with mid-absorption isf changes, it will be t-10h to t) - /// - carbRatio: Carb ratio timeline: t-10h to t+6h + /// - sensitivityMgdlPerUnit: Insulin sensitivity timeline in mg/dL per unit: t-16h to t + /// - carbRatio: Carb ratio timeline in grams per unit: t-10h to t+6h /// - targetMgdl: Correction range timeline in mg/dL. Used only for the integral-correction clamp when `emulation.integralRCClamp` is enabled; may be empty otherwise. /// - algorithmEffectsOptions: Which effects to include when combining effects to generate glucose prediction /// - useIntegralRetrospectiveCorrection: If true, the prediction will use Integral Retrospection. If false, will use traditional Retrospective Correction /// - includingPositiveVelocityAndRC: If false, only net negative momentum and RC effects will used. + /// - useMidAbsorptionISF: If true, each dose's effect follows the sensitivity timeline over its absorption instead of using the sensitivity at delivery. /// - carbAbsorptionModel: A model conforming to CarbAbsorptionComputable that is used for computing carb absorption over time. - /// - emulation: Options for reproducing earlier deployed algorithm behavior; the default emulates nothing. + /// - gradualTransitionsThreshold: Largest jump in mg/dL between consecutive glucose readings for retrospective correction to apply. /// - momentumVelocityMaximumMgdlPerMinute: Limit on the momentum effect, in mg/dL per minute + /// - emulation: Options for reproducing earlier deployed algorithm behavior; the default emulates nothing. + /// - experiments: Algorithm experiments to apply; the empty set is standard LoopAlgorithm. /// - Returns: A LoopPrediction struct containing the predicted glucose and the computed intermediate effects used to make the prediction /// public static func generatePrediction( @@ -446,7 +446,9 @@ public struct Algorithm { /// `[t - lookback, t + 6h]` for future-insulin mode). /// - carbEntries: History of carb entries. /// - sensitivityMgdlPerUnit: ISF timeline in mg/dL per unit — still required for carb + RC effects. - /// - carbRatio: Carb ratio timeline. + /// - carbRatio: Carb ratio timeline in grams per unit. + /// - targetMgdl: Correction range timeline in mg/dL. Used only for the integral-correction clamp when `emulation.integralRCClamp` is enabled; may be empty otherwise. + /// - scheduledBasalRate: Scheduled basal rate in U/h at `start`, for the integral-correction clamp. /// - algorithmEffectsOptions: Which effects to include. /// - useIntegralRetrospectiveCorrection: Use integral RC. /// - includingPositiveVelocityAndRC: Include positive velocity/RC. @@ -454,6 +456,8 @@ public struct Algorithm { /// `precomputedInsulin.insulinEffects` is non-nil). /// - carbAbsorptionModel: Carb absorption model. /// - gradualTransitionsThreshold: RC smoothness gate (default 40 mg/dL). + /// - momentumVelocityMaximumMgdlPerMinute: Limit on the momentum effect, in mg/dL per minute. + /// - emulation: Options for reproducing earlier deployed algorithm behavior; the default emulates nothing. /// - Returns: A `LoopPrediction` struct. `dosesRelativeToBasal` is /// populated from `precomputedInsulin.annotatedDoses`. public static func generatePrediction( @@ -806,6 +810,13 @@ public struct Algorithm { return bolus } + /// Runs the algorithm: forecasts glucose and recommends a dose. + /// + /// Inputs are plain numbers in fixed units: glucose and targets in mg/dL, sensitivity in mg/dL per unit, + /// carbs in grams, carb ratio in grams per unit, insulin in units and rates in U/h. See `AlgorithmInput`. + /// + /// - Parameter input: The glucose, dose and carb history, therapy settings and options. + /// - Returns: The forecast, the effects behind it, and the recommendation or the error that prevented one. public static func run(input: LoopAlgorithmInputType) -> AlgorithmOutput { let scheduledSensitivity = input.sensitivityQuantities diff --git a/Sources/LoopAlgorithm/LoopPredictionInput.swift b/Sources/LoopAlgorithm/LoopPredictionInput.swift index 69414f6..579a7ce 100644 --- a/Sources/LoopAlgorithm/LoopPredictionInput.swift +++ b/Sources/LoopAlgorithm/LoopPredictionInput.swift @@ -21,7 +21,7 @@ public struct LoopPredictionInput] - // Expected time range coverage: t-16h to t (eventually with mid-absorption isf changes, it will be t-10h to t) + // Expected time range coverage: t-16h to t // Insulin sensitivity in mg/dL per unit public var sensitivityMgdlPerUnit: [AbsoluteScheduleValue] diff --git a/Sources/LoopAlgorithm/SampleValue.swift b/Sources/LoopAlgorithm/SampleValue.swift index 775a80e..1085ecd 100644 --- a/Sources/LoopAlgorithm/SampleValue.swift +++ b/Sources/LoopAlgorithm/SampleValue.swift @@ -90,8 +90,7 @@ public extension Sequence where Element: TimelineValue { This behavior mimics HKQueryOptionNone, where the value must merely overlap the specified range, not strictly exist inside of it. - - parameter startDate: The earliest date of elements to return - - parameter endDate: The latest date of elements to return + - parameter interval: The date interval elements must overlap - returns: A new array of elements */