From de296779f96a02a98d077e4560c4819a6f250bd7 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 13 Oct 2026 11:34:10 -0500 Subject: [PATCH 1/2] Record algorithm experiments on dosing decisions Loop, remote recommendation and manual bolus decisions record which algorithm experiments were in effect: integral retrospective correction, and glucose based partial application, which only applies with the automatic bolus strategy. Loop and remote recommendation decisions also record the application factor it chose. --- Loop/Managers/LoopDataManager.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 43c4dca721..9a4b871693 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -693,6 +693,7 @@ final class LoopDataManager: ObservableObject { dosingStrategy = settingsProvider.settings.automaticDosingStrategy } input.recommendationType = dosingStrategy.recommendationType + dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy, applicationFactor: input.automaticBolusApplicationFactor) guard let latestGlucose = input.glucoseHistory.last else { throw LoopError.missingDataError(.glucose) @@ -889,6 +890,7 @@ final class LoopDataManager: ObservableObject { if let output = displayState.output { var dosingDecision = StoredDosingDecision(date: now, reason: "updateRemoteRecommendation") + dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, applicationFactor: displayState.input?.automaticBolusApplicationFactor) dosingDecision.predictedGlucose = output.predictedGlucose dosingDecision.insulinOnBoard = displayState.activeInsulin dosingDecision.carbsOnBoard = displayState.activeCarbs @@ -1025,8 +1027,22 @@ extension LoopDataManager { } } + private var currentDosingStrategy: AutomaticDosingStrategy { + dosingStrategySelectionEnabled ? settingsProvider.settings.automaticDosingStrategy : .automaticBolus + } + + /// Glucose based partial application only affects automatic boluses, so it is in effect only with that strategy + private func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy, applicationFactor: Double?) -> StoredDosingDecision.AlgorithmExperiments { + let glucoseBasedApplicationFactor = UserDefaults.standard.glucoseBasedApplicationFactorEnabled && dosingStrategy == .automaticBolus + return StoredDosingDecision.AlgorithmExperiments( + integralRetrospectiveCorrection: UserDefaults.standard.integralRetrospectiveCorrectionEnabled, + glucoseBasedApplicationFactor: glucoseBasedApplicationFactor, + applicationFactor: glucoseBasedApplicationFactor ? applicationFactor : nil + ) + } + func storeManualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) async { - let dosingDecision = StoredDosingDecision(id: bolusDosingDecision.id, + var dosingDecision = StoredDosingDecision(id: bolusDosingDecision.id, date: date, reason: bolusDosingDecision.reason.rawValue, settings: StoredDosingDecision.Settings(settingsProvider.settings), @@ -1045,6 +1061,7 @@ extension LoopDataManager { predictedGlucose: bolusDosingDecision.predictedGlucose, manualBolusRecommendation: bolusDosingDecision.manualBolusRecommendation, manualBolusRequested: bolusDosingDecision.manualBolusRequested) + dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, applicationFactor: nil) Task { await dosingDecisionStore.storeDosingDecision(dosingDecision) } } From 0eca7da9be5ee8e079f6b33aef5ccda9fcfe87af Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 13 Oct 2026 11:55:47 -0500 Subject: [PATCH 2/2] Record algorithm experiments as identifiers Decisions list integralRetrospectiveCorrection and glucoseBasedApplicationFactor as AlgorithmExperiment identifiers when in effect, with no parameters yet; neither has user configuration. The application factor is no longer recorded. --- Loop/Managers/LoopDataManager.swift | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 9a4b871693..df086d0bcd 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -693,7 +693,7 @@ final class LoopDataManager: ObservableObject { dosingStrategy = settingsProvider.settings.automaticDosingStrategy } input.recommendationType = dosingStrategy.recommendationType - dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy, applicationFactor: input.automaticBolusApplicationFactor) + dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy) guard let latestGlucose = input.glucoseHistory.last else { throw LoopError.missingDataError(.glucose) @@ -890,7 +890,7 @@ final class LoopDataManager: ObservableObject { if let output = displayState.output { var dosingDecision = StoredDosingDecision(date: now, reason: "updateRemoteRecommendation") - dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, applicationFactor: displayState.input?.automaticBolusApplicationFactor) + dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy) dosingDecision.predictedGlucose = output.predictedGlucose dosingDecision.insulinOnBoard = displayState.activeInsulin dosingDecision.carbsOnBoard = displayState.activeCarbs @@ -1032,13 +1032,15 @@ extension LoopDataManager { } /// Glucose based partial application only affects automatic boluses, so it is in effect only with that strategy - private func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy, applicationFactor: Double?) -> StoredDosingDecision.AlgorithmExperiments { - let glucoseBasedApplicationFactor = UserDefaults.standard.glucoseBasedApplicationFactorEnabled && dosingStrategy == .automaticBolus - return StoredDosingDecision.AlgorithmExperiments( - integralRetrospectiveCorrection: UserDefaults.standard.integralRetrospectiveCorrectionEnabled, - glucoseBasedApplicationFactor: glucoseBasedApplicationFactor, - applicationFactor: glucoseBasedApplicationFactor ? applicationFactor : nil - ) + private func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy) -> [AlgorithmExperiment] { + var experiments: [AlgorithmExperiment] = [] + if UserDefaults.standard.integralRetrospectiveCorrectionEnabled { + experiments.append(AlgorithmExperiment(identifier: "integralRetrospectiveCorrection")) + } + if UserDefaults.standard.glucoseBasedApplicationFactorEnabled && dosingStrategy == .automaticBolus { + experiments.append(AlgorithmExperiment(identifier: "glucoseBasedApplicationFactor")) + } + return experiments } func storeManualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) async { @@ -1061,7 +1063,7 @@ extension LoopDataManager { predictedGlucose: bolusDosingDecision.predictedGlucose, manualBolusRecommendation: bolusDosingDecision.manualBolusRecommendation, manualBolusRequested: bolusDosingDecision.manualBolusRequested) - dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, applicationFactor: nil) + dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy) Task { await dosingDecisionStore.storeDosingDecision(dosingDecision) } }