Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommendation Breakdown and Auto Bolus Carbs - PR against main #2277

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0883440
Recommend for carbs only, excluding corrections
Dec 28, 2023
d16fbea
Support carb prediction for testing recommendations
Dec 29, 2023
d4ba097
Add feature flag to control whether used or not
Dec 30, 2023
742352d
Enable recommendation bolus breakdown
Dec 31, 2023
332f2e3
Use chevron icon
Jan 1, 2024
ac655eb
Fix spacing issue
Jan 1, 2024
1464933
Work in progress for breakdown
Jan 2, 2024
6c61b0a
Fix up calculations. Add support for max bolus to breakdown
Jan 3, 2024
27d6485
Merge pull request #12 from motinis/temp-breakdown
motinis Jan 3, 2024
9600aa6
Add unit tests
Jan 3, 2024
8bcfcb0
Fixup correction calc when clamped by max bolus
Jan 3, 2024
eb785de
Round to 2 fractional digits in UI
Jan 3, 2024
b66ce5b
Change icon for bidi languages
Jan 3, 2024
2ce4777
Add support for Limit to 0 Bolus
Jan 4, 2024
ed552c7
Don't calc correction when in range
Jan 4, 2024
2e5a709
Don't calc correction when in range
Jan 4, 2024
816fb6a
initial update for cob and bg correction split
Mar 23, 2024
0538ac7
Merge branch 'LoopKit:dev' into bolus-breakdown-cob
motinis Mar 23, 2024
01b8917
refine COB calculation
Mar 24, 2024
2145564
initial unit testing
Mar 25, 2024
b2d568c
Updates to tests, add controls to UI
Mar 27, 2024
8d88f77
Support both maxBolusExcess and safetyLimit
Mar 28, 2024
9ba70fe
Fix rounding mode
Mar 29, 2024
1aaee2d
Bump version to 3.5.0 to signify dev branch
ps2 Jul 13, 2024
e8a17e5
Merge pull request #13 from LoopKit/dev
motinis Aug 2, 2024
7522d54
Fixes
Aug 23, 2024
79efd20
Separate out recommendation from UI interaction. Fix slowdown too
Aug 23, 2024
a3696e7
Add Diable BG Correction options. Only use carbs and insulin for max …
Aug 24, 2024
546d25c
Handle negative insulin better for COB
Aug 28, 2024
f4345d9
in progress
Sep 23, 2024
afceef4
Unify code paths. Tests not yet passing
Oct 10, 2024
4d1d0df
After refactoring. Tests passing
Oct 11, 2024
20409ad
Add comment and minor generalization
Oct 11, 2024
ae6fb73
Add test for missing insulin due to suspend threshold effects on dosage
Oct 11, 2024
8c2a86d
Fix display calculations for max and safety limits
Oct 13, 2024
7d781ee
Merge branch 'main-custom' into bolus-breakdown-cob
motinis Oct 25, 2024
f208d25
Add Auto-Bolus Carbs Experiment
Dec 26, 2024
621973d
Add missing tests and fix beneath correction range
Dec 26, 2024
b6dff3f
Support automaticDosingIOBLimit for ABC
Jan 5, 2025
e1c2382
Force reload from UserDefaults
Jan 6, 2025
4eb434d
Use AppStorage instead
Jan 7, 2025
9e4c32a
Fix potential rounding issues that can occur with temp basals
Jan 7, 2025
1ee8993
Refactor so to optimize ABC
Jan 7, 2025
08f45c6
Use AppStorage
Jan 21, 2025
a4d9fcb
Update to reflect appstorage use
Jan 21, 2025
9ba803e
Under Development Section + Exclusions
Jan 21, 2025
abb2927
Update display on change to AlgorithmExperiments
Jan 21, 2025
ad7b0ea
Style missing insulin rows when there are exclusions
Jan 23, 2025
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
Prev Previous commit
Next Next commit
initial update for cob and bg correction split
Moti Nisenson-Ken committed Mar 23, 2024
commit 816fb6a6e750c072aee1ffb9c9d41a450c41477d
7 changes: 0 additions & 7 deletions Common/FeatureFlags.swift
Original file line number Diff line number Diff line change
@@ -39,7 +39,6 @@ struct FeatureFlagConfiguration: Decodable {
let profileExpirationSettingsViewEnabled: Bool
let missedMealNotifications: Bool
let allowAlgorithmExperiments: Bool
let recommendBolusForCarbsWithoutCorrection: Bool


fileprivate init() {
@@ -233,12 +232,6 @@ struct FeatureFlagConfiguration: Decodable {
#else
self.allowAlgorithmExperiments = false
#endif

#if RECOMMEND_BOLUS_FOR_CARBS_WITHOUT_CORRECTION
self.recommendBolusForCarbsWithoutCorrection = true
#else
self.recommendBolusForCarbsWithoutCorrection = false
#endif
}
}

99 changes: 67 additions & 32 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
@@ -1474,57 +1474,69 @@ extension LoopDataManager {
guard recommendation != nil else {
return nil
}

let carbBreakdownRecommendation = try recommendBolusValidatingDataRecency(forPrediction: prediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .carbBreakdown)

let cobPrediction = try predictGlucose(using: [.carbs, .insulin], potentialBolus: nil, potentialCarbEntry: potentialCarbEntry, replacingCarbEntry: replacedCarbEntry, includingPendingInsulin: shouldIncludePendingInsulin, includingPositiveVelocityAndRC: false)

let totalCobAmount : Double

if let cobBreakdownRecommendation = try recommendBolusValidatingDataRecency(forPrediction: cobPrediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .cobBreakdown) {

totalCobAmount = cobBreakdownRecommendation.amount
} else {
return recommendation // unable to differentiate between correction amounts, this generally shouldn't happen
}


let carbBreakdownRecommendation = try recommendBolusValidatingDataRecency(forPrediction: cobPrediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .carbBreakdown)

guard carbBreakdownRecommendation != nil else {
if potentialCarbEntry != nil {
return recommendation // unable to differentiate between carbs and correction
}
return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: 0.0, correctionAmount: recommendation!.amount, missingAmount: recommendation!.missingAmount)
let carbsAmount = potentialCarbEntry == nil ? 0.0 : nil
let bgAmount = recommendation!.amount - totalCobAmount

return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: carbsAmount, cobCorrectionAmount: totalCobAmount, bgCorrectionAmount: bgAmount, missingAmount: recommendation!.missingAmount)
}

guard potentialCarbEntry != nil else {
let extra = Swift.max(recommendation!.missingAmount ?? 0, 0)

var correctionAmount = recommendation!.amount + extra

if (correctionAmount == 0) {
let correctionBreakdownRecommendation = try recommendBolusValidatingDataRecency(forPrediction: prediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .correctionBreakdown)

if correctionBreakdownRecommendation != nil {
correctionAmount = calcCorrectionAmount(carbsAmount: 0, carbBreakdownRecommendation: carbBreakdownRecommendation!, correctionBreakdownRecommendation: correctionBreakdownRecommendation!)
if correctionAmount == 0 {
if let calcAmount = try calcCorrectionAmount(carbsAmount: 0, prediction: prediction, potentialCarbEntry: potentialCarbEntry) {

correctionAmount = calcAmount
if recommendation!.notice == .predictedGlucoseInRange {
correctionAmount = Swift.min(correctionAmount, 0) // ensure 0 if in range but above the mid-point
}
} else {
correctionAmount = 0
}
}

return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: 0.0, correctionAmount: correctionAmount, missingAmount: recommendation!.missingAmount)
let bgAmount = correctionAmount - totalCobAmount

return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: 0.0, cobCorrectionAmount: totalCobAmount, bgCorrectionAmount: bgAmount, missingAmount: recommendation!.missingAmount)
}

let zeroCarbEntry = replacedCarbEntry == nil ? nil : NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 1E-50), startDate: potentialCarbEntry!.startDate, foodType: nil, absorptionTime: potentialCarbEntry!.absorptionTime)

let predictionWithoutCarbs = try predictGlucose(using: .all, potentialBolus: nil, includingPendingInsulin: shouldIncludePendingInsulin, includingPositiveVelocityAndRC: considerPositiveVelocityAndRC)
let predictionWithoutCarbs = try predictGlucose(using: [.carbs, .insulin], potentialBolus: nil, potentialCarbEntry: zeroCarbEntry, replacingCarbEntry: replacedCarbEntry, includingPendingInsulin: shouldIncludePendingInsulin, includingPositiveVelocityAndRC: false)

let carbBreakdownRecommendationWithoutCarbs = try recommendBolusValidatingDataRecency(forPrediction: predictionWithoutCarbs, consideringPotentialCarbEntry: nil, usage: .carbBreakdown)
let carbBreakdownRecommendationWithoutCarbs = try recommendBolusValidatingDataRecency(forPrediction: predictionWithoutCarbs, consideringPotentialCarbEntry: zeroCarbEntry, usage: .carbBreakdown)

guard carbBreakdownRecommendationWithoutCarbs != nil else {
return recommendation // unable to directly calculate carbsAmount
}

let carbsAmount = carbBreakdownRecommendation!.amount - carbBreakdownRecommendationWithoutCarbs!.amount
let cobAmount = max(totalCobAmount - carbsAmount, 0)

let correctionBreakdownRecommendation = try recommendBolusValidatingDataRecency(forPrediction: prediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .correctionBreakdown)

var missingAmount = recommendation!.missingAmount
let correctionAmount : Double

if recommendation!.amount <= 0 && correctionBreakdownRecommendation != nil && recommendation!.notice != .predictedGlucoseInRange {
correctionAmount = calcCorrectionAmount(carbsAmount: carbsAmount, carbBreakdownRecommendation: carbBreakdownRecommendation!, correctionBreakdownRecommendation: correctionBreakdownRecommendation!)
if recommendation!.amount <= 0, recommendation!.notice != .predictedGlucoseInRange,
let calcAmount = try calcCorrectionAmount(carbsAmount: carbsAmount, prediction: prediction, potentialCarbEntry: potentialCarbEntry) {

correctionAmount = calcAmount
let amount = carbsAmount + correctionAmount
if volumeRounder()(amount) != 0 {
missingAmount = amount
@@ -1533,30 +1545,44 @@ extension LoopDataManager {
let extra = Swift.max(recommendation!.missingAmount ?? 0, 0)
correctionAmount = recommendation!.amount + extra - carbsAmount
}

return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: carbsAmount, correctionAmount: correctionAmount, missingAmount: missingAmount)
return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: carbsAmount, cobCorrectionAmount: cobAmount, bgCorrectionAmount: correctionAmount - cobAmount, missingAmount: missingAmount)
}

fileprivate func calcCorrectionAmount(carbsAmount: Double,
carbBreakdownRecommendation : ManualBolusRecommendation,
correctionBreakdownRecommendation: ManualBolusRecommendation) -> Double {
prediction: [PredictedGlucoseValue],
potentialCarbEntry: NewCarbEntry?) throws -> Double? {

let recommendationAmountForCarbs = try recommendBolusValidatingDataRecency(forPrediction: prediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .carbBreakdown)?.amount

guard recommendationAmountForCarbs != nil else {
return nil
}

let recommendationAmountForCorrection = try recommendBolusValidatingDataRecency(forPrediction: prediction, consideringPotentialCarbEntry: potentialCarbEntry, usage: .correctionBreakdown)?.amount

guard recommendationAmountForCorrection != nil else {
return nil
}

// carbs + correction + y = a
// carbs + correction + ratio*y = b
// --> y = (b-a)/(ratio - 1)
// --> correction = a - y - carbs

let ratio = ManualBolusRecommendationUsage.correctionBreakdown.targetsAdjustment / ManualBolusRecommendationUsage.carbBreakdown.targetsAdjustment
let delta = correctionBreakdownRecommendation.amount - carbBreakdownRecommendation.amount
let y = (recommendationAmountForCorrection! - recommendationAmountForCarbs!) / (ratio - 1)

return carbBreakdownRecommendation.amount - delta / (ratio - 1) - carbsAmount
return recommendationAmountForCarbs! - y - carbsAmount
}

fileprivate enum ManualBolusRecommendationUsage {
case standard, carbBreakdown, correctionBreakdown
case standard, cobBreakdown, carbBreakdown, correctionBreakdown

func suspendThresholdOverride(_ suspendThreshold: HKQuantity?) -> HKQuantity? {
switch self {
case .standard: return suspendThreshold
case .cobBreakdown: return HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: -1E30)
default: return nil
}
}
@@ -1578,14 +1604,19 @@ extension LoopDataManager {
var targetsAdjustment : Double {
switch self {
case .standard: return 0.0
case .cobBreakdown: return 0.0
case .carbBreakdown: return -1E5
case .correctionBreakdown: return -2E5
}
}

func glucoseTargetsOverride(_ schedule: GlucoseRangeSchedule) -> GlucoseRangeSchedule{
func glucoseTargetsOverride(_ schedule: GlucoseRangeSchedule, _ startingGlucose: HKQuantity) -> GlucoseRangeSchedule{
switch self {
case .standard: return schedule
case .cobBreakdown:
let target = startingGlucose.doubleValue(for: .milligramsPerDeciliter)
return GlucoseRangeSchedule(unit: .milligramsPerDeciliter,
dailyItems: [RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: target, maxValue: target))])!
default: return adjustSchedule(schedule, amount: self.targetsAdjustment)
}
}
@@ -1659,10 +1690,6 @@ extension LoopDataManager {
guard let insulinSensitivity = insulinSensitivityScheduleApplyingOverrideHistory else {
throw LoopError.configurationError(.insulinSensitivitySchedule)
}

let breakdownGlucoseRangeSchedule = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [
RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: -1E5, maxValue: -1E5))
])

guard let glucoseTargetRange = settings.effectiveGlucoseTargetRangeSchedule(presumingMealEntry: potentialCarbEntry != nil) else {
throw LoopError.configurationError(.glucoseTargetRangeSchedule)
@@ -1681,8 +1708,16 @@ extension LoopDataManager {

let model = doseStore.insulinModelProvider.model(for: pumpInsulinType)

var startingGlucose : HKQuantity

if let latestGlucose = self.glucoseStore.latestGlucose?.quantity {
startingGlucose = latestGlucose
} else {
startingGlucose = predictedGlucose[0].quantity
}

return predictedGlucose.recommendedManualBolus(
to: usage.glucoseTargetsOverride(glucoseTargetRange),
to: usage.glucoseTargetsOverride(glucoseTargetRange, startingGlucose),
at: now(),
suspendThreshold: usage.suspendThresholdOverride(settings.suspendThreshold?.quantity),
sensitivity: insulinSensitivity,
41 changes: 29 additions & 12 deletions Loop/View Models/BolusEntryViewModel.swift
Original file line number Diff line number Diff line change
@@ -117,9 +117,13 @@ final class BolusEntryViewModel: ObservableObject {
var carbBolusAmount: Double? {
carbBolus?.doubleValue(for: .internationalUnit())
}
@Published var correctionBolus: HKQuantity?
var correctionBolusAmount: Double? {
correctionBolus?.doubleValue(for: .internationalUnit())
@Published var cobCorrectionBolus: HKQuantity?
var cobCorrectionBolusAmount: Double? {
cobCorrectionBolus?.doubleValue(for: .internationalUnit())
}
@Published var bgCorrectionBolus: HKQuantity?
var bgCorrectionBolusAmount: Double? {
bgCorrectionBolus?.doubleValue(for: .internationalUnit())
}
@Published var missingBolus: HKQuantity?
var missingBolusAmount: Double? {
@@ -676,7 +680,8 @@ final class BolusEntryViewModel: ObservableObject {
let now = Date()
var recommendation: ManualBolusRecommendation?
let carbBolus: HKQuantity?
let correctionBolus: HKQuantity?
let cobCorrectionBolus: HKQuantity?
let bgCorrectionBolus: HKQuantity?
let recommendedBolus: HKQuantity?
let missingBolus: HKQuantity?
let notice: Notice?
@@ -690,10 +695,16 @@ final class BolusEntryViewModel: ObservableObject {
carbBolus = nil
}

if let correctionAmount = recommendation.correctionAmount {
correctionBolus = HKQuantity(unit: .internationalUnit(), doubleValue: correctionAmount)
if let cobCorrectionAmount = recommendation.cobCorrectionAmount {
cobCorrectionBolus = HKQuantity(unit: .internationalUnit(), doubleValue: cobCorrectionAmount)
} else {
correctionBolus = nil
cobCorrectionBolus = nil
}

if let bgCorrectionAmount = recommendation.bgCorrectionAmount {
bgCorrectionBolus = HKQuantity(unit: .internationalUnit(), doubleValue: bgCorrectionAmount)
} else {
bgCorrectionBolus = nil
}

if let missingAmount = recommendation.missingAmount {
@@ -725,14 +736,16 @@ final class BolusEntryViewModel: ObservableObject {
}
} else {
carbBolus = nil
correctionBolus = nil
cobCorrectionBolus = nil
bgCorrectionBolus = nil
missingBolus = nil
recommendedBolus = HKQuantity(unit: .internationalUnit(), doubleValue: 0)
notice = nil
}
} catch {
carbBolus = nil
correctionBolus = nil
cobCorrectionBolus = nil
bgCorrectionBolus = nil
missingBolus = nil
recommendedBolus = nil

@@ -751,7 +764,8 @@ final class BolusEntryViewModel: ObservableObject {
DispatchQueue.main.async {
let priorRecommendedBolus = self.recommendedBolus
self.carbBolus = carbBolus
self.correctionBolus = correctionBolus
self.cobCorrectionBolus = cobCorrectionBolus
self.bgCorrectionBolus = bgCorrectionBolus
self.missingBolus = missingBolus
self.recommendedBolus = recommendedBolus
self.dosingDecision.manualBolusRecommendation = recommendation.map { ManualBolusRecommendationWithDate(recommendation: $0, date: now) }
@@ -850,8 +864,11 @@ final class BolusEntryViewModel: ObservableObject {
var carbBolusString: String {
return bolusString(carbBolusAmount, forBreakdown: true)
}
var correctionBolusString: String {
return bolusString(correctionBolusAmount, forBreakdown: true)
var cobCorrectionBolusString: String {
return bolusString(cobCorrectionBolusAmount, forBreakdown: true)
}
var bgCorrectionBolusString: String {
return bolusString(bgCorrectionBolusAmount, forBreakdown: true)
}
var negativeMissingBolusString: String {
guard missingBolusAmount != nil else {
27 changes: 21 additions & 6 deletions Loop/Views/BolusEntryView.swift
Original file line number Diff line number Diff line change
@@ -229,9 +229,9 @@ struct BolusEntryView: View {

private func displayRecommendationBreakdown() -> Bool {
if viewModel.potentialCarbEntry != nil {
return viewModel.carbBolus != nil && viewModel.correctionBolus != nil
return viewModel.bgCorrectionBolus != nil && (viewModel.carbBolus != nil || viewModel.cobCorrectionBolus != nil)
} else {
return viewModel.correctionBolus != nil
return viewModel.bgCorrectionBolus != nil
}
}

@@ -269,7 +269,7 @@ struct BolusEntryView: View {
if viewModel.potentialCarbEntry != nil && viewModel.carbBolus != nil {
HStack {
Text(" ")
Text("Carb Bolus", comment: "Label for carb bolus row on bolus screen")
Text("Carb Entry Bolus", comment: "Label for carb bolus row on bolus screen")
.font(.footnote)
Spacer()
HStack(alignment: .firstTextBaseline) {
@@ -281,14 +281,29 @@ struct BolusEntryView: View {
}
.accessibilityElement(children: .combine)
}
if viewModel.correctionBolus != nil {
if viewModel.cobCorrectionBolus != nil {
HStack {
Text(" ")
Text("Correction Bolus", comment: "Label for correction bolus row on bolus screen")
Text("COB Correction Bolus", comment: "Label for COB correction bolus row on bolus screen")
.font(.footnote)
Spacer()
HStack(alignment: .firstTextBaseline) {
Text(viewModel.correctionBolusString)
Text(viewModel.cobCorrectionBolusString)
.font(.footnote)
.foregroundColor(Color(.label))
breakdownBolusUnitsLabel
}
}
.accessibilityElement(children: .combine)
}
if viewModel.bgCorrectionBolus != nil {
HStack {
Text(" ")
Text("BG Correction Bolus", comment: "Label for BG correction bolus row on bolus screen")
.font(.footnote)
Spacer()
HStack(alignment: .firstTextBaseline) {
Text(viewModel.bgCorrectionBolusString)
.font(.footnote)
.foregroundColor(Color(.label))
breakdownBolusUnitsLabel
Loading