Skip to content

Commit a46eacc

Browse files
authored
nate/feature/LOOP-232/clipping-predicted-glucose-values (#196)
* initial pass at clipping the predicted glucose values * reanaming as suggested in PR comment * correcting typo * refactored to allow the predicted glucose bounds to be configurable * allow the user to see the predicted glucose values are determined by the algorithm without clamping when the user taps the glucose chart * do not display the eventual glucose for the status glucose chart * clean up * added feature flag to enable the predicted glucose chart clamp * clean up * responses to PR comments * minor clean-up * responses to PR comments * corrected typo
1 parent 14e9887 commit a46eacc

File tree

7 files changed

+130
-61
lines changed

7 files changed

+130
-61
lines changed

Common/FeatureFlags.swift

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,81 @@ import Foundation
1111
let FeatureFlags = FeatureFlagConfiguration()
1212

1313
struct FeatureFlagConfiguration: Decodable {
14-
let sensitivityOverridesEnabled: Bool
14+
let criticalAlertsEnabled: Bool
15+
let deleteAllButtonEnabled: Bool
16+
let fiaspInsulinModelEnabled: Bool
17+
let includeServicesInSettingsEnabled: Bool
18+
let mockTherapySettingsEnabled: Bool
1519
let nonlinearCarbModelEnabled: Bool
20+
let observeHealthKitSamplesFromOtherApps: Bool
1621
let remoteOverridesEnabled: Bool
17-
let criticalAlertsEnabled: Bool
22+
let predictedGlucoseChartClampEnabled: Bool
1823
let scenariosEnabled: Bool
24+
let sensitivityOverridesEnabled: Bool
1925
let simulatedCoreDataEnabled: Bool
2026
let walshInsulinModelEnabled: Bool
21-
let fiaspInsulinModelEnabled: Bool
22-
let observeHealthKitSamplesFromOtherApps: Bool
23-
let includeServicesInSettingsEnabled: Bool
24-
let mockTherapySettingsEnabled: Bool
25-
let deleteAllButtonEnabled: Bool
2627

2728
fileprivate init() {
29+
#if CRITICAL_ALERTS_ENABLED
30+
self.criticalAlertsEnabled = true
31+
#else
32+
self.criticalAlertsEnabled = false
33+
#endif
34+
35+
// Swift compiler config is inverse, since the default state is enabled.
36+
#if DELETE_ALL_BUTTON_DISABLED
37+
self.deleteAllButtonEnabled = false
38+
#else
39+
self.deleteAllButtonEnabled = true
40+
#endif
41+
2842
// Swift compiler config is inverse, since the default state is enabled.
2943
#if FEATURE_OVERRIDES_DISABLED
3044
self.sensitivityOverridesEnabled = false
3145
#else
3246
self.sensitivityOverridesEnabled = true
3347
#endif
3448

49+
// Swift compiler config is inverse, since the default state is enabled.
50+
#if FIASP_INSULIN_MODEL_DISABLED
51+
self.fiaspInsulinModelEnabled = false
52+
#else
53+
self.fiaspInsulinModelEnabled = true
54+
#endif
55+
56+
// Swift compiler config is inverse, since the default state is enabled.
57+
#if INCLUDE_SERVICES_IN_SETTINGS_DISABLED
58+
self.includeServicesInSettingsEnabled = false
59+
#else
60+
self.includeServicesInSettingsEnabled = true
61+
#endif
62+
63+
// Swift compiler config is inverse, since the default state is enabled.
64+
#if MOCK_THERAPY_SETTINGS_ENABLED
65+
self.mockTherapySettingsEnabled = true
66+
#else
67+
self.mockTherapySettingsEnabled = false
68+
#endif
69+
3570
// Swift compiler config is inverse, since the default state is enabled.
3671
#if NONLINEAR_CARB_MODEL_DISABLED
3772
self.nonlinearCarbModelEnabled = false
3873
#else
3974
self.nonlinearCarbModelEnabled = true
4075
#endif
76+
77+
// Swift compiler config is inverse, since the default state is enabled.
78+
#if OBSERVE_HEALTH_KIT_SAMPLES_FROM_OTHER_APPS_DISABLED
79+
self.observeHealthKitSamplesFromOtherApps = false
80+
#else
81+
self.observeHealthKitSamplesFromOtherApps = true
82+
#endif
83+
84+
#if PREDICTED_GLUCOSE_CHART_CLAMP_ENABLED
85+
self.predictedGlucoseChartClampEnabled = true
86+
#else
87+
self.predictedGlucoseChartClampEnabled = false
88+
#endif
4189

4290
// Swift compiler config is inverse, since the default state is enabled.
4391
#if REMOTE_OVERRIDES_DISABLED
@@ -46,12 +94,6 @@ struct FeatureFlagConfiguration: Decodable {
4694
self.remoteOverridesEnabled = true
4795
#endif
4896

49-
#if CRITICAL_ALERTS_ENABLED
50-
self.criticalAlertsEnabled = true
51-
#else
52-
self.criticalAlertsEnabled = false
53-
#endif
54-
5597
#if SCENARIOS_ENABLED
5698
self.scenariosEnabled = true
5799
#else
@@ -70,59 +112,26 @@ struct FeatureFlagConfiguration: Decodable {
70112
#else
71113
self.walshInsulinModelEnabled = true
72114
#endif
73-
74-
// Swift compiler config is inverse, since the default state is enabled.
75-
#if FIASP_INSULIN_MODEL_DISABLED
76-
self.fiaspInsulinModelEnabled = false
77-
#else
78-
self.fiaspInsulinModelEnabled = true
79-
#endif
80-
81-
// Swift compiler config is inverse, since the default state is enabled.
82-
#if OBSERVE_HEALTH_KIT_SAMPLES_FROM_OTHER_APPS_DISABLED
83-
self.observeHealthKitSamplesFromOtherApps = false
84-
#else
85-
self.observeHealthKitSamplesFromOtherApps = true
86-
#endif
87-
88-
// Swift compiler config is inverse, since the default state is enabled.
89-
#if INCLUDE_SERVICES_IN_SETTINGS_DISABLED
90-
self.includeServicesInSettingsEnabled = false
91-
#else
92-
self.includeServicesInSettingsEnabled = true
93-
#endif
94-
95-
#if MOCK_THERAPY_SETTINGS_ENABLED
96-
self.mockTherapySettingsEnabled = true
97-
#else
98-
self.mockTherapySettingsEnabled = false
99-
#endif
100-
101-
// Swift compiler config is inverse, since the default state is enabled.
102-
#if DELETE_ALL_BUTTON_DISABLED
103-
self.deleteAllButtonEnabled = false
104-
#else
105-
self.deleteAllButtonEnabled = true
106-
#endif
107115
}
108116
}
109117

110118

111119
extension FeatureFlagConfiguration : CustomDebugStringConvertible {
112120
var debugDescription: String {
113121
return [
114-
"* sensitivityOverridesEnabled: \(sensitivityOverridesEnabled)",
115-
"* nonlinearCarbModelEnabled: \(nonlinearCarbModelEnabled)",
116-
"* remoteOverridesEnabled: \(remoteOverridesEnabled)",
117122
"* criticalAlertsEnabled: \(criticalAlertsEnabled)",
118-
"* scenariosEnabled: \(scenariosEnabled)",
119-
"* simulatedCoreDataEnabled: \(simulatedCoreDataEnabled)",
120-
"* walshInsulinModelEnabled: \(walshInsulinModelEnabled)",
123+
"* deleteAllButtonEnabled: \(deleteAllButtonEnabled)",
121124
"* fiaspInsulinModelEnabled: \(fiaspInsulinModelEnabled)",
122-
"* observeHealthKitSamplesFromOtherApps: \(observeHealthKitSamplesFromOtherApps)",
123125
"* includeServicesInSettingsEnabled: \(includeServicesInSettingsEnabled)",
124126
"* mockTherapySettingsEnabled: \(mockTherapySettingsEnabled)",
125-
"* deleteAllButtonEnabled: \(deleteAllButtonEnabled)"
127+
"* nonlinearCarbModelEnabled: \(nonlinearCarbModelEnabled)",
128+
"* observeHealthKitSamplesFromOtherApps: \(observeHealthKitSamplesFromOtherApps)",
129+
"* predictedGlucoseChartClampEnabled: \(predictedGlucoseChartClampEnabled)",
130+
"* remoteOverridesEnabled: \(remoteOverridesEnabled)",
131+
"* scenariosEnabled: \(scenariosEnabled)",
132+
"* sensitivityOverridesEnabled: \(sensitivityOverridesEnabled)",
133+
"* simulatedCoreDataEnabled: \(simulatedCoreDataEnabled)",
134+
"* walshInsulinModelEnabled: \(walshInsulinModelEnabled)"
126135
].joined(separator: "\n")
127136
}
128137
}

Loop Status Extension/StatusChartsManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import SwiftCharts
1212
import UIKit
1313

1414
class StatusChartsManager: ChartsManager {
15-
let predictedGlucose = PredictedGlucoseChart()
15+
let predictedGlucose = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil)
1616

1717
init(colors: ChartColorPalette, settings: ChartSettings, traitCollection: UITraitCollection) {
1818
super.init(colors: colors, settings: settings, charts: [predictedGlucose], traitCollection: traitCollection)

Loop/Managers/StatusChartsManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class StatusChartsManager: ChartsManager {
2626
let cob: COBChart
2727

2828
init(colors: ChartColorPalette, settings: ChartSettings, traitCollection: UITraitCollection) {
29-
let glucose = PredictedGlucoseChart()
29+
let glucose = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil)
3030
let iob = IOBChart()
3131
let dose = DoseChart()
3232
let cob = COBChart()

Loop/View Controllers/PredictionTableViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable
9494
}
9595
}
9696

97-
let glucoseChart = PredictedGlucoseChart()
97+
let glucoseChart = PredictedGlucoseChart(predictedGlucoseBounds: nil)
9898

9999
override func createChartsManager() -> ChartsManager {
100100
return ChartsManager(colors: .primary, settings: .default, charts: [glucoseChart], traitCollection: traitCollection)

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,12 @@ final class StatusTableViewController: LoopChartsTableViewController {
450450
if let predictedGlucoseValues = predictedGlucoseValues {
451451
self.statusCharts.setPredictedGlucoseValues(predictedGlucoseValues)
452452
}
453-
if let lastPoint = self.statusCharts.glucose.predictedGlucosePoints.last?.y {
453+
if !FeatureFlags.predictedGlucoseChartClampEnabled,
454+
let lastPoint = self.statusCharts.glucose.predictedGlucosePoints.last?.y
455+
{
454456
self.eventualGlucoseDescription = String(describing: lastPoint)
455457
} else {
458+
// if the predicted glucose values are clamped, the eventually glucose description should not be displayed, since it may not align with what is being charted.
456459
self.eventualGlucoseDescription = nil
457460
}
458461
if currentContext.contains(.targets) {

Loop/View Models/BolusEntryViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ final class BolusEntryViewModel: ObservableObject {
7272
private var cancellables: Set<AnyCancellable> = []
7373

7474
let chartManager: ChartsManager = {
75-
let predictedGlucoseChart = PredictedGlucoseChart()
75+
let predictedGlucoseChart = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil)
7676
predictedGlucoseChart.glucoseDisplayRange = BolusEntryViewModel.defaultGlucoseDisplayRange
7777
return ChartsManager(colors: .primary, settings: .default, charts: [predictedGlucoseChart], traitCollection: .current)
7878
}()

LoopUI/Charts/PredictedGlucoseChart.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Foundation
99
import LoopKit
1010
import LoopKitUI
1111
import SwiftCharts
12+
import HealthKit
1213

1314
public class PredictedGlucoseChart: GlucoseChart, ChartProviding {
1415

@@ -66,11 +67,18 @@ public class PredictedGlucoseChart: GlucoseChart, ChartProviding {
6667

6768
public private(set) var endDate: Date?
6869

70+
private var predictedGlucoseSoftBounds: PredictedGlucoseBounds?
71+
6972
private func updateEndDate(_ date: Date) {
7073
if endDate == nil || date > endDate! {
7174
self.endDate = date
7275
}
7376
}
77+
78+
public init(predictedGlucoseBounds: PredictedGlucoseBounds?) {
79+
self.predictedGlucoseSoftBounds = predictedGlucoseBounds
80+
super.init()
81+
}
7482
}
7583

7684
extension PredictedGlucoseChart {
@@ -245,10 +253,59 @@ extension PredictedGlucoseChart {
245253
}
246254

247255
public func setPredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) {
248-
predictedGlucosePoints = glucosePointsFromValues(glucoseValues)
256+
let clampedPredicatedGlucoseValues = clampPredictedGlucoseValues(glucoseValues)
257+
predictedGlucosePoints = glucosePointsFromValues(clampedPredicatedGlucoseValues)
249258
}
250259

251260
public func setAlternatePredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) {
252261
alternatePredictedGlucosePoints = glucosePointsFromValues(glucoseValues)
253262
}
254263
}
264+
265+
266+
// MARK: - Clamping the predicted glucose values
267+
extension PredictedGlucoseChart {
268+
var chartedGlucoseValueMaximum: HKQuantity? {
269+
guard let glucosePointMaximum = glucosePoints.max(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
270+
return nil
271+
}
272+
return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMaximum.y.scalar)
273+
}
274+
275+
var chartedGlucoseValueMinimum: HKQuantity? {
276+
guard let glucosePointMinimum = glucosePoints.min(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
277+
return nil
278+
}
279+
return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMinimum.y.scalar)
280+
}
281+
282+
func clampPredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) -> [GlucoseValue] {
283+
guard let predictedGlucoseBounds = predictedGlucoseSoftBounds else {
284+
return glucoseValues
285+
}
286+
287+
let predictedGlucoseValueMaximum = chartedGlucoseValueMaximum != nil ? max(predictedGlucoseBounds.maximum, chartedGlucoseValueMaximum!) : predictedGlucoseBounds.maximum
288+
289+
let predictedGlucoseValueMinimum = chartedGlucoseValueMinimum != nil ? min(predictedGlucoseBounds.minimum, chartedGlucoseValueMinimum!) : predictedGlucoseBounds.minimum
290+
291+
return glucoseValues.map {
292+
if $0.quantity > predictedGlucoseValueMaximum {
293+
return PredictedGlucoseValue(startDate: $0.startDate, quantity: predictedGlucoseValueMaximum)
294+
} else if $0.quantity < predictedGlucoseValueMinimum {
295+
return PredictedGlucoseValue(startDate: $0.startDate, quantity: predictedGlucoseValueMinimum)
296+
} else {
297+
return $0
298+
}
299+
}
300+
}
301+
302+
public struct PredictedGlucoseBounds {
303+
var minimum: HKQuantity
304+
var maximum: HKQuantity
305+
306+
public static var `default`: PredictedGlucoseBounds {
307+
return PredictedGlucoseBounds(minimum: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 40),
308+
maximum: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 400))
309+
}
310+
}
311+
}

0 commit comments

Comments
 (0)