diff --git a/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift b/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift index 9987980..444e977 100644 --- a/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift +++ b/Sources/LoopAlgorithm/Glucose/GlucoseMath.swift @@ -79,6 +79,10 @@ extension BidirectionalCollection where Element: GlucoseSampleValue, Index == In /// - gradualTransitionThreshold: Maximum allowed difference between consecutive readings in mg/dL (default 40.0) /// - Returns: True if all consecutive differences are within the threshold public func hasGradualTransitions(gradualTransitionThreshold: Double = 40.0) -> Bool { + if contains(where: \.wasUserEntered) { + return filter { !$0.wasUserEntered }.hasGradualTransitions(gradualTransitionThreshold: gradualTransitionThreshold) + } + guard count > 1 else { return false // A single point could be a spike and should not be used for momentum calculation } @@ -114,6 +118,10 @@ extension BidirectionalCollection where Element: GlucoseSampleValue, Index == In velocityMaximum: LoopQuantity? = nil ) -> [GlucoseEffect] { + if contains(where: \.wasUserEntered) { + return filter { !$0.wasUserEntered }.linearMomentumEffect(duration: duration, delta: delta, velocityMaximum: velocityMaximum) + } + let velocityMax = velocityMaximum ?? LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 4.0) guard @@ -176,6 +184,11 @@ extension Collection where Element: GlucoseSampleValue, Index == Int { /// - Parameter effects: Glucose effects to be countered, in chronological order /// - Returns: An array of velocities describing the change in glucose samples compared to the specified effects public func counteractionEffects(to effects: [GlucoseEffect]) -> [GlucoseEffectVelocity] { + // A manual entry is a different measurement from the CGM; a change between the two is not glucose movement. + if contains(where: \.wasUserEntered) { + return filter { !$0.wasUserEntered }.counteractionEffects(to: effects) + } + let mgdL = LoopUnit.milligramsPerDeciliter let velocityUnit = GlucoseEffectVelocity.perSecondUnit var velocities = [GlucoseEffectVelocity]() diff --git a/Tests/LoopAlgorithmTests/GlucoseMathTests.swift b/Tests/LoopAlgorithmTests/GlucoseMathTests.swift index ee346ba..8cb1a4f 100644 --- a/Tests/LoopAlgorithmTests/GlucoseMathTests.swift +++ b/Tests/LoopAlgorithmTests/GlucoseMathTests.swift @@ -422,6 +422,52 @@ final class GlucoseMathTests: XCTestCase { } } + private func manualEntry(at date: Date, mgdl: Double) -> GlucoseFixtureValue { + GlucoseFixtureValue( + startDate: date, + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl), + isDisplayOnly: false, + wasUserEntered: true, + provenanceIdentifier: nil, + condition: nil, + trendRate: nil + ) + } + + func testMomentumEffectIgnoresManualEntry() { + var input = loadInputFixture("momentum_effect_falling_glucose_input") + let expected = input.linearMomentumEffect(duration: .minutes(30)) + let middle = input[input.count / 2] + input.insert(manualEntry(at: middle.startDate.addingTimeInterval(.minutes(2.5)), mgdl: middle.quantity.doubleValue(for: .milligramsPerDeciliter) - 30), at: input.count / 2 + 1) + + XCTAssertEqual(expected, input.linearMomentumEffect(duration: .minutes(30))) + } + + func testCounteractionEffectsIgnoreManualEntry() { + var input = loadInputFixture("counteraction_effect_falling_glucose_input") + let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin") + let expected = input.counteractionEffects(to: insulinEffect) + let index = input.count / 2 + let sample = input[index] + input.insert(manualEntry(at: sample.startDate.addingTimeInterval(.minutes(4.5)), mgdl: sample.quantity.doubleValue(for: .milligramsPerDeciliter) - 30), at: index + 1) + + let effects = input.counteractionEffects(to: insulinEffect) + let unit = GlucoseEffectVelocity.perSecondUnit + + XCTAssertEqual(expected.map(\.startDate), effects.map(\.startDate)) + XCTAssertEqual(expected.map(\.endDate), effects.map(\.endDate)) + XCTAssertEqual(expected.map { $0.quantity.doubleValue(for: unit) }, effects.map { $0.quantity.doubleValue(for: unit) }) + } + + func testGradualTransitionsIgnoreManualEntry() { + var input = loadInputFixture("momentum_effect_falling_glucose_input") + XCTAssertTrue(input.hasGradualTransitions()) + let middle = input[input.count / 2] + input.insert(manualEntry(at: middle.startDate.addingTimeInterval(.minutes(2.5)), mgdl: middle.quantity.doubleValue(for: .milligramsPerDeciliter) + 60), at: input.count / 2 + 1) + + XCTAssertTrue(input.hasGradualTransitions()) + } + func testCounteractionEffectsForNoGlucose() { let input = [GlucoseFixtureValue]() let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")