Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@
D6E28676C2C7751C77C7F2FB /* Loop/Resources/Sounds/spring.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9471E156FC10D6D0FF219C77 /* Loop/Resources/Sounds/spring.caf */; };
DD3DBD292A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */; };
A1B2C3D42E8F000100000002 /* GuardAndRunwaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42E8F000100000001 /* GuardAndRunwaySelectionView.swift */; };
883675B93130EED04D819D0A /* SlowRetrospectiveCorrectionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1622757183D5C361AFB3FB5C /* SlowRetrospectiveCorrectionSelectionView.swift */; };
DDC389FC2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */; };
DDC389FE2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */; };
DE965737FE2DAE9FE692769E /* Loop/Resources/Sounds/alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0FC34B523FE5A50C920506D5 /* Loop/Resources/Sounds/alarm.caf */; };
Expand Down Expand Up @@ -1538,6 +1539,7 @@
CE102E23D8C1C1291EF66387 /* PluginPickerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PluginPickerView.swift; sourceTree = "<group>"; };
DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegralRetrospectiveCorrectionSelectionView.swift; sourceTree = "<group>"; };
A1B2C3D42E8F000100000001 /* GuardAndRunwaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuardAndRunwaySelectionView.swift; sourceTree = "<group>"; };
1622757183D5C361AFB3FB5C /* SlowRetrospectiveCorrectionSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlowRetrospectiveCorrectionSelectionView.swift; sourceTree = "<group>"; };
DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+algorithmExperimentsSection.swift"; sourceTree = "<group>"; };
DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBasedApplicationFactorSelectionView.swift; sourceTree = "<group>"; };
E4854073AAE5C2CB97167F63 /* HealthAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthAccessView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2211,6 +2213,7 @@
DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */,
DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */,
A1B2C3D42E8F000100000001 /* GuardAndRunwaySelectionView.swift */,
1622757183D5C361AFB3FB5C /* SlowRetrospectiveCorrectionSelectionView.swift */,
84DEB10C2C18FABA00170734 /* IOSFocusModesView.swift */,
84D1F1A62D09053A00CB271F /* StatusTableView.swift */,
B429CAB22E97C7F300FA988E /* LoopStatusModalView.swift */,
Expand Down Expand Up @@ -3774,6 +3777,7 @@
414E7082E6A6B9DA8CD11EF3 /* CurrentSetupViewModel.swift in Sources */,
DD3DBD292A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift in Sources */,
A1B2C3D42E8F000100000002 /* GuardAndRunwaySelectionView.swift in Sources */,
883675B93130EED04D819D0A /* SlowRetrospectiveCorrectionSelectionView.swift in Sources */,
842E40A72F22F7E2000CCCE0 /* TintedContent.swift in Sources */,
842E40A92F22F7E2000CCCE0 /* EstimatedReadTime.swift in Sources */,
842E40AA2F22F7E2000CCCE0 /* PresetsTrainingView.swift in Sources */,
Expand Down
12 changes: 10 additions & 2 deletions Loop/Managers/AnalyticsServicesManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ final class AnalyticsServicesManager {
identify("Integral Retrospective Correction", value: state.integralRetrospectiveCorrection ? "On" : "Off")
identify("Glucose Based Partial Application", value: state.glucoseBasedPartialApplication ? "On" : "Off")
identify("Guard + Runway", value: state.guardAndRunway ? "On" : "Off")
identify("Slow Retrospective Correction", value: state.slowRetrospectiveCorrection ? "On" : "Off")
lastState = state
}

Expand Down Expand Up @@ -353,6 +354,9 @@ final class AnalyticsServicesManager {
if newState.guardAndRunway != oldState.guardAndRunway {
logEvent("Algorithm experiment change", withProperties: ["experiment": "Guard + Runway", "enabled": newState.guardAndRunway])
}
if newState.slowRetrospectiveCorrection != oldState.slowRetrospectiveCorrection {
logEvent("Algorithm experiment change", withProperties: ["experiment": "Slow Retrospective Correction", "enabled": newState.slowRetrospectiveCorrection])
}
}

func didDisplayAlert(identifier: String, interruptionLevel: Alert.InterruptionLevel, scheduled: Bool) {
Expand Down Expand Up @@ -415,23 +419,26 @@ struct AnalyticsState: Codable, Equatable {
var integralRetrospectiveCorrection: Bool
var glucoseBasedPartialApplication: Bool
var guardAndRunway: Bool
var slowRetrospectiveCorrection: Bool

init(closedLoop: Bool, dosingStrategy: String, integralRetrospectiveCorrection: Bool, glucoseBasedPartialApplication: Bool, guardAndRunway: Bool) {
init(closedLoop: Bool, dosingStrategy: String, integralRetrospectiveCorrection: Bool, glucoseBasedPartialApplication: Bool, guardAndRunway: Bool, slowRetrospectiveCorrection: Bool) {
self.closedLoop = closedLoop
self.dosingStrategy = dosingStrategy
self.integralRetrospectiveCorrection = integralRetrospectiveCorrection
self.glucoseBasedPartialApplication = glucoseBasedPartialApplication
self.guardAndRunway = guardAndRunway
self.slowRetrospectiveCorrection = slowRetrospectiveCorrection
}

// Summaries stored before guardAndRunway was added lack it
// Summaries stored before guardAndRunway or slowRetrospectiveCorrection was added lack them
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
closedLoop = try container.decode(Bool.self, forKey: .closedLoop)
dosingStrategy = try container.decode(String.self, forKey: .dosingStrategy)
integralRetrospectiveCorrection = try container.decode(Bool.self, forKey: .integralRetrospectiveCorrection)
glucoseBasedPartialApplication = try container.decode(Bool.self, forKey: .glucoseBasedPartialApplication)
guardAndRunway = try container.decodeIfPresent(Bool.self, forKey: .guardAndRunway) ?? false
slowRetrospectiveCorrection = try container.decodeIfPresent(Bool.self, forKey: .slowRetrospectiveCorrection) ?? false
}
}

Expand Down Expand Up @@ -481,6 +488,7 @@ struct AnalyticsDailySummary: Codable, Equatable {
properties["integralRetrospectiveCorrection"] = state.integralRetrospectiveCorrection
properties["glucoseBasedPartialApplication"] = state.glucoseBasedPartialApplication
properties["guardAndRunway"] = state.guardAndRunway
properties["slowRetrospectiveCorrection"] = state.slowRetrospectiveCorrection
}
properties.merge(usage.properties) { current, _ in current }
return properties
Expand Down
3 changes: 2 additions & 1 deletion Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,8 @@ class LoopAppManager: NSObject {
dosingStrategy: settingsManager.loopSettings.automaticDosingStrategy.analyticsValue,
integralRetrospectiveCorrection: UserDefaults.standard.integralRetrospectiveCorrectionEnabled,
glucoseBasedPartialApplication: UserDefaults.standard.glucoseBasedApplicationFactorEnabled,
guardAndRunway: UserDefaults.standard.guardAndRunwayEnabled
guardAndRunway: UserDefaults.standard.guardAndRunwayEnabled,
slowRetrospectiveCorrection: UserDefaults.standard.slowRetrospectiveCorrectionEnabled
)
}
analyticsServicesManager.identifyState()
Expand Down
85 changes: 61 additions & 24 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ struct AlgorithmDisplayState {
var input: StoredDataAlgorithmInput?
var output: AlgorithmOutput<StoredCarbEntry>?

/// The ISF multiplier slow retrospective correction applied on the last run, 1 when it is off.
var sensitivityMultiplier: Double {
output?.slowRetrospectiveCorrection?.multiplier ?? 1
}

var activeInsulin: InsulinValue? {
guard let input, let value = output?.activeInsulin else {
return nil
Expand Down Expand Up @@ -366,8 +371,17 @@ final class LoopDataManager: ObservableObject {
ensureCarbCoverageStart: Date? = nil,
projectOngoingDoses: Bool = false
) async throws -> StoredDataAlgorithmInput {
let experiments = enabledAlgorithmExperiments

// Slow retrospective correction keeps no state, so it reads its whole window of glucose on every run,
// with the carbs and doses that explain it.
let slowRetrospectiveCorrectionHistory: TimeInterval? = experiments.contains(.slowRetrospectiveCorrection) ? .hours(72) : nil

// Need to fetch doses back as far as t - (DIA + DCA) for Dynamic carbs
let dosesInputHistory = CarbMath.maximumAbsorptionTimeInterval + InsulinMath.defaultInsulinActivityDuration
var dosesInputHistory = CarbMath.maximumAbsorptionTimeInterval + InsulinMath.defaultInsulinActivityDuration
if let slowRetrospectiveCorrectionHistory {
dosesInputHistory = max(dosesInputHistory, slowRetrospectiveCorrectionHistory + InsulinMath.defaultInsulinActivityDuration)
}

let baseTime = baseTime ?? now

Expand Down Expand Up @@ -417,7 +431,12 @@ final class LoopDataManager: ObservableObject {

let forecastEndTime = baseTime.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta)

let carbsStart = baseTime.addingTimeInterval(LoopConstants.maxCarbEntryPastTime + .minutes(-1)) // additional minute to handle difference in seconds between carb entry and carb ratio
var glucoseStart = baseTime.addingTimeInterval(LoopConstants.maxCarbEntryPastTime + .minutes(-1)) // additional minute to handle difference in seconds between carb entry and carb ratio
var carbsStart = glucoseStart
if let slowRetrospectiveCorrectionHistory {
glucoseStart = baseTime.addingTimeInterval(-slowRetrospectiveCorrectionHistory)
carbsStart = glucoseStart.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval + .minutes(-1))
}
// A pending carb entry can outlive this rolling window while the bolus screen is open.
// Cover it with schedule history without expanding the stored-carb or glucose queries.
let carbScheduleStart = min(carbsStart, ensureCarbCoverageStart ?? carbsStart)
Expand All @@ -439,7 +458,7 @@ final class LoopDataManager: ObservableObject {
throw LoopError.configurationError(.carbRatioSchedule)
}

let glucose = try await glucoseStore.getGlucoseSamples(start: carbsStart, end: baseTime)
let glucose = try await glucoseStore.getGlucoseSamples(start: glucoseStart, end: baseTime)

let dosesWithModel = doses.map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) }

Expand Down Expand Up @@ -542,7 +561,6 @@ final class LoopDataManager: ObservableObject {
}


let experiments = enabledAlgorithmExperiments
let volatilityBaseline = experiments.contains(.guardAndRunway) ? await currentVolatilityBaseline(at: baseTime) : nil

return StoredDataAlgorithmInput(
Expand Down Expand Up @@ -578,6 +596,9 @@ final class LoopDataManager: ObservableObject {
if UserDefaults.standard.guardAndRunwayEnabled {
experiments.insert(.guardAndRunway)
}
if UserDefaults.standard.slowRetrospectiveCorrectionEnabled {
experiments.insert(.slowRetrospectiveCorrection)
}
return experiments
}

Expand Down Expand Up @@ -719,7 +740,7 @@ final class LoopDataManager: ObservableObject {
}

var output = Algorithm.run(input: input)
dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy, guardAndRunway: output.guardAndRunway)
dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy, output: output)

switch output.recommendationResult {
case .success(let recommendation):
Expand Down Expand Up @@ -924,7 +945,7 @@ final class LoopDataManager: ObservableObject {

if let output = displayState.output {
var dosingDecision = StoredDosingDecision(date: now, reason: "updateRemoteRecommendation")
dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, guardAndRunway: output.guardAndRunway)
dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, output: output)
dosingDecision.predictedGlucose = output.predictedGlucose.map(\.loopKit)
dosingDecision.insulinOnBoard = displayState.activeInsulin
dosingDecision.carbsOnBoard = displayState.activeCarbs
Expand Down Expand Up @@ -1065,30 +1086,43 @@ 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, guardAndRunway: GuardAndRunwayOutput? = nil) -> [AlgorithmExperiment] {
private func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy, output: AlgorithmOutput<StoredCarbEntry>? = nil) -> [AlgorithmExperiment] {
enabledAlgorithmExperiments
.filter { $0 != .glucoseBasedApplicationFactor || dosingStrategy == .automaticBolus }
.sorted { $0.rawValue < $1.rawValue }
.map { experiment in
guard experiment == .guardAndRunway else {
switch experiment {
case .guardAndRunway:
return guardAndRunwayExperiment(output?.guardAndRunway)
case .slowRetrospectiveCorrection:
var parameters: [String: JSONValue] = [:]
if let slow = output?.slowRetrospectiveCorrection {
parameters["level"] = .number(slow.level)
parameters["multiplier"] = .number(slow.multiplier)
}
return AlgorithmExperiment(identifier: experiment.rawValue, parameters: parameters.isEmpty ? nil : .object(parameters))
default:
return AlgorithmExperiment(identifier: experiment.rawValue)
}
var parameters: [String: JSONValue] = [:]
if let baseline = volatilityBaseline?.value {
parameters["volatilityBaseline"] = .number(baseline)
}
if let guardAndRunway {
parameters["volatility"] = .number(guardAndRunway.volatility)
parameters["bandApplied"] = .bool(guardAndRunway.bandApplied)
parameters["bandGain"] = .number(guardAndRunway.bandGain)
parameters["bandDepth"] = .number(guardAndRunway.bandDepth)
parameters["runwayShift"] = .number(guardAndRunway.runwayShift)
parameters["postLowRiseCutActive"] = .bool(guardAndRunway.postLowRiseCutActive)
}
return AlgorithmExperiment(identifier: experiment.rawValue, parameters: parameters.isEmpty ? nil : .object(parameters))
}
}

private func guardAndRunwayExperiment(_ guardAndRunway: GuardAndRunwayOutput?) -> AlgorithmExperiment {
var parameters: [String: JSONValue] = ["version": .number(2)]
if let baseline = volatilityBaseline?.value {
parameters["volatilityBaseline"] = .number(baseline)
}
if let guardAndRunway {
parameters["volatility"] = .number(guardAndRunway.volatility)
parameters["bandApplied"] = .bool(guardAndRunway.bandApplied)
parameters["bandGain"] = .number(guardAndRunway.bandGain)
parameters["bandDepth"] = .number(guardAndRunway.bandDepth)
parameters["runwayShift"] = .number(guardAndRunway.runwayShift)
parameters["postLowRiseCutActive"] = .bool(guardAndRunway.postLowRiseCutActive)
}
return AlgorithmExperiment(identifier: ExperimentOption.guardAndRunway.rawValue, parameters: .object(parameters))
}

/// The median σ5 over the last four weeks, for guard + runway's volatility band. Nil until there is a day of glucose.
private func currentVolatilityBaseline(at date: Date) async -> Double? {
if let volatilityBaseline, abs(date.timeIntervalSince(volatilityBaseline.date)) < (volatilityBaseline.value == nil ? .hours(1) : .days(1)) {
Expand Down Expand Up @@ -1347,15 +1381,18 @@ extension StoredDataAlgorithmInput {
}
}

func predictGlucose(effectsOptions: AlgorithmEffectsOptions = .all) throws -> [LoopAlgorithm.PredictedGlucoseValue] {
/// `sensitivityMultiplier` is slow retrospective correction's, from the algorithm's last run.
func predictGlucose(effectsOptions: AlgorithmEffectsOptions = .all, sensitivityMultiplier: Double = 1) throws -> [LoopAlgorithm.PredictedGlucoseValue] {
try validateCarbScheduleCoverage()
let prediction = Algorithm.generatePrediction(
start: predictionStart,
glucoseHistory: glucoseHistory,
doses: doses,
carbEntries: carbEntries,
basal: basal,
sensitivityMgdlPerUnit: sensitivityMgdlPerUnit,
sensitivityMgdlPerUnit: sensitivityMgdlPerUnit.map {
LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value * sensitivityMultiplier)
},
carbRatio: carbRatio,
algorithmEffectsOptions: effectsOptions,
useIntegralRetrospectiveCorrection: self.useIntegralRetrospectiveCorrection,
Expand Down Expand Up @@ -1589,7 +1626,7 @@ extension LoopDataManager: BolusEntryViewModelDelegate {
.removingCarbEntry(carbEntry: carbEntryChange?.originalEntry)
.addingCarbEntry(carbEntry: carbEntryChange?.asStoredCarbEntry)

let prediction = try input.predictGlucose()
let prediction = try input.predictGlucose(sensitivityMultiplier: displayState.sensitivityMultiplier)

return (historicGlucose: input.glucoseHistory, predictedGlucose: prediction.map(\.loopKit))
}
Expand Down
Loading