From 70df3609fdff823f820015321bb070b7a0bebc63 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 25 Oct 2026 10:45:11 -0500 Subject: [PATCH] Report the band's actual depth Loop leaves out forecast points where nothing changes, so a forecast may have no point near 60 min, where the band is deepest. bandDepth is now the most the band lowered any point of the forecast, not the band's depth at 60 min. --- .../LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift | 8 +++----- Sources/LoopAlgorithm/LoopAlgorithm.swift | 9 +++++++-- Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift | 3 +-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift b/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift index 5afe682..2c80b00 100644 --- a/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift +++ b/Sources/LoopAlgorithm/Experiments/GuardAndRunwayOutput.swift @@ -13,6 +13,9 @@ public struct GuardAndRunwayOutput: Equatable { public var volatility: Double /// False when active carbs kept the volatility band off. public var bandApplied: Bool + /// The most the band lowered any point of the forecast, mg/dL; 0 when it was off. A forecast with no + /// points near 60 min, where the band is deepest, is lowered less. + public var bandDepth: Double /// How far the target for automatic dosing was lowered, mg/dL. Manual recommendations keep the target. public var runwayShift: Double /// True while the post-low cut scales down the rising side of retrospective correction. @@ -23,11 +26,6 @@ public struct GuardAndRunwayOutput: Equatable { /// The band's depth multiplier, from the volatility baseline. public var bandGain: Double { band.gain } - /// How far the band lowers the forecast at its deepest point (60 min), mg/dL; 0 when it was off. - public var bandDepth: Double { - bandApplied ? band.gain * volatility * pow(band.horizon.minutes / 5.0, band.exponent) : 0 - } - /// `glucose` lowered by the band as this run applied it; unchanged when the band was off. public func guarded(_ glucose: [PredictedGlucoseValue]) -> [PredictedGlucoseValue] { bandApplied ? LoopAlgorithm.volatilityBanded(glucose, band: band, volatility: volatility) : glucose diff --git a/Sources/LoopAlgorithm/LoopAlgorithm.swift b/Sources/LoopAlgorithm/LoopAlgorithm.swift index e5a3fa1..378f0e5 100644 --- a/Sources/LoopAlgorithm/LoopAlgorithm.swift +++ b/Sources/LoopAlgorithm/LoopAlgorithm.swift @@ -842,11 +842,16 @@ public struct LoopAlgorithm { } if let band = experimentParameters.volatilityBand, let volatility { let bandApplied = !(band.carbGate && (prediction.activeCarbs ?? 0) > 0) + var bandDepth = 0.0 if bandApplied { - prediction.glucose = volatilityBanded(prediction.glucose, band: band, volatility: volatility) + let banded = volatilityBanded(prediction.glucose, band: band, volatility: volatility) + bandDepth = zip(prediction.glucose, banded).map { + $0.quantity.doubleValue(for: .milligramsPerDeciliter) - $1.quantity.doubleValue(for: .milligramsPerDeciliter) + }.max() ?? 0 + prediction.glucose = banded } guardAndRunway = GuardAndRunwayOutput( - volatility: volatility, bandApplied: bandApplied, runwayShift: runwayShift, + volatility: volatility, bandApplied: bandApplied, bandDepth: bandDepth, runwayShift: runwayShift, postLowRiseCutActive: experimentParameters.postLowRiseCut?.isActive(glucoseHistory: input.glucoseHistory) ?? false, band: band) } diff --git a/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift b/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift index 5ea3fa9..45ab34b 100644 --- a/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift +++ b/Tests/LoopAlgorithmTests/AlgorithmExperimentsTests.swift @@ -248,8 +248,7 @@ final class AlgorithmExperimentsTests: XCTestCase { XCTAssertFalse(output.postLowRiseCutActive) XCTAssertEqual(output.guarded(standard.predictedGlucose), runway.predictedGlucose) let deepest = zip(plain, banded).map { $0 - $1 }.max()! - XCTAssertLessThanOrEqual(deepest, output.bandDepth + 1e-9) - XCTAssertGreaterThan(deepest, 0.9 * output.bandDepth, "forecast points fall near the 60 min horizon") + XCTAssertEqual(output.bandDepth, deepest, accuracy: 1e-9) if type == .tempBasal { XCTAssertGreaterThan(runway.recommendation!.automatic!.basalAdjustment.unitsPerHour, standard.recommendation!.automatic!.basalAdjustment.unitsPerHour)