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
146 changes: 127 additions & 19 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1456,7 +1456,7 @@ extension LoopDataManager {
let prediction = try predictGlucoseFromManualGlucose(glucose, potentialBolus: nil, potentialCarbEntry: potentialCarbEntry, replacingCarbEntry: replacedCarbEntry, includingPendingInsulin: shouldIncludePendingInsulin, considerPositiveVelocityAndRC: considerPositiveVelocityAndRC)
return try recommendManualBolus(forPrediction: prediction, consideringPotentialCarbEntry: potentialCarbEntry)
}

/// - Throws: LoopError.missingDataError
fileprivate func recommendBolus(consideringPotentialCarbEntry potentialCarbEntry: NewCarbEntry?, replacingCarbEntry replacedCarbEntry: StoredCarbEntry?, considerPositiveVelocityAndRC: Bool) throws -> ManualBolusRecommendation? {
guard lastRequestedBolus == nil else {
Expand All @@ -1465,7 +1465,7 @@ extension LoopDataManager {
// successful in any case.
return nil
}

let pendingInsulin = try getPendingInsulin()
let shouldIncludePendingInsulin = pendingInsulin > 0
let prediction = try predictGlucose(using: .all, potentialBolus: nil, potentialCarbEntry: potentialCarbEntry, replacingCarbEntry: replacedCarbEntry, includingPendingInsulin: shouldIncludePendingInsulin, includingPositiveVelocityAndRC: considerPositiveVelocityAndRC)
Expand All @@ -1474,30 +1474,132 @@ extension LoopDataManager {
guard recommendation != nil else {
return nil
}

let carbBreakdownRecommendation = try recommendBolusValidatingDataRecency(forPrediction: prediction, 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)
}

guard potentialCarbEntry != nil else {
return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: 0.0, correctionAmount: recommendation!.amount)
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!)
} else {
correctionAmount = 0
}
}

return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: 0.0, correctionAmount: correctionAmount, missingAmount: recommendation!.missingAmount)
}


let predictionWithoutCarbs = try predictGlucose(using: .all, potentialBolus: nil, includingPendingInsulin: shouldIncludePendingInsulin, includingPositiveVelocityAndRC: considerPositiveVelocityAndRC)

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

guard recommendationWithoutCarbs != nil else {
return recommendation // unable to differentiate between carbs and correction
guard carbBreakdownRecommendationWithoutCarbs != nil else {
return recommendation // unable to directly calculate carbsAmount
}

return ManualBolusRecommendation(amount: recommendation!.amount, pendingInsulin: recommendation!.pendingInsulin, notice: recommendation!.notice, carbsAmount: recommendation!.amount - recommendationWithoutCarbs!.amount, correctionAmount: recommendationWithoutCarbs!.amount)

let carbsAmount = carbBreakdownRecommendation!.amount - carbBreakdownRecommendationWithoutCarbs!.amount

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

let correctionAmount : Double

if recommendation!.amount <= 0 && correctionBreakdownRecommendation != nil {
correctionAmount = calcCorrectionAmount(carbsAmount: carbsAmount, carbBreakdownRecommendation: carbBreakdownRecommendation!, correctionBreakdownRecommendation: correctionBreakdownRecommendation!)
} else {
correctionAmount = recommendation!.amount - carbsAmount
}

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

fileprivate func calcCorrectionAmount(carbsAmount: Double,
carbBreakdownRecommendation : ManualBolusRecommendation,
correctionBreakdownRecommendation: ManualBolusRecommendation) -> Double {
// 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

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

fileprivate enum ManualBolusRecommendationUsage {
case standard, carbBreakdown, correctionBreakdown

func suspendThresholdOverride(_ suspendThreshold: HKQuantity?) -> HKQuantity? {
switch self {
case .standard: return suspendThreshold
default: return nil
}
}

func maxBolusOverride(_ maxBolus: Double) -> Double {
switch self {
case .standard: return maxBolus
default: return 1E15
}
}

func volumeRounderOverride(_ volumeRounder: @escaping (Double) -> Double) -> ((Double) -> Double)? {
switch self {
case .standard: return volumeRounder
default: return nil
}
}

var targetsAdjustment : Double {
switch self {
case .standard: return 0.0
case .carbBreakdown: return -1E5
case .correctionBreakdown: return -2E5
}
}

func glucoseTargetsOverride(_ schedule: GlucoseRangeSchedule) -> GlucoseRangeSchedule{
switch self {
case .standard: return schedule
default: return adjustSchedule(schedule, amount: self.targetsAdjustment)
}
}

private func adjustSchedule(_ schedule: GlucoseRangeSchedule, amount: Double) -> GlucoseRangeSchedule {
return GlucoseRangeSchedule(unit: schedule.unit,
dailyItems: schedule.items.map{scheduleValue in
scheduleValue.map{range in
DoubleRange(minValue: range.minValue + amount, maxValue: range.maxValue + amount)}},
timeZone: schedule.timeZone)!
}


}


/// - Throws:
/// - LoopError.missingDataError
/// - LoopError.glucoseTooOld
/// - LoopError.invalidFutureGlucose
/// - LoopError.pumpDataTooOld
/// - LoopError.configurationError
fileprivate func recommendBolusValidatingDataRecency<Sample: GlucoseValue>(forPrediction predictedGlucose: [Sample],
consideringPotentialCarbEntry potentialCarbEntry: NewCarbEntry?) throws -> ManualBolusRecommendation? {
consideringPotentialCarbEntry potentialCarbEntry: NewCarbEntry?,
usage: ManualBolusRecommendationUsage = .standard) throws -> ManualBolusRecommendation? {
guard let glucose = glucoseStore.latestGlucose else {
throw LoopError.missingDataError(.glucose)
}
Expand Down Expand Up @@ -1529,7 +1631,7 @@ extension LoopDataManager {
throw LoopError.missingDataError(.insulinEffect)
}

return try recommendManualBolus(forPrediction: predictedGlucose, consideringPotentialCarbEntry: potentialCarbEntry)
return try recommendManualBolus(forPrediction: predictedGlucose, consideringPotentialCarbEntry: potentialCarbEntry, usage: usage)
}

private func volumeRounder() -> ((Double) -> Double) {
Expand All @@ -1541,13 +1643,19 @@ extension LoopDataManager {

/// - Throws: LoopError.configurationError
private func recommendManualBolus<Sample: GlucoseValue>(forPrediction predictedGlucose: [Sample],
consideringPotentialCarbEntry potentialCarbEntry: NewCarbEntry?) throws -> ManualBolusRecommendation? {
guard let glucoseTargetRange = settings.effectiveGlucoseTargetRangeSchedule(presumingMealEntry: potentialCarbEntry != nil) else {
throw LoopError.configurationError(.glucoseTargetRangeSchedule)
}
consideringPotentialCarbEntry potentialCarbEntry: NewCarbEntry?,
usage: ManualBolusRecommendationUsage = .standard) throws -> ManualBolusRecommendation? {
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)
}
guard let maxBolus = settings.maximumBolus else {
throw LoopError.configurationError(.maximumBolus)
}
Expand All @@ -1561,16 +1669,16 @@ extension LoopDataManager {
}

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

return predictedGlucose.recommendedManualBolus(
to: glucoseTargetRange,
to: usage.glucoseTargetsOverride(glucoseTargetRange),
at: now(),
suspendThreshold: settings.suspendThreshold?.quantity,
suspendThreshold: usage.suspendThresholdOverride(settings.suspendThreshold?.quantity),
sensitivity: insulinSensitivity,
model: model,
pendingInsulin: 0, // Pending insulin is already reflected in the prediction
maxBolus: maxBolus,
volumeRounder: volumeRounder()
maxBolus: usage.maxBolusOverride(maxBolus),
volumeRounder: usage.volumeRounderOverride(volumeRounder())
)
}

Expand Down
49 changes: 40 additions & 9 deletions Loop/View Models/BolusEntryViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ final class BolusEntryViewModel: ObservableObject {
var correctionBolusAmount: Double? {
correctionBolus?.doubleValue(for: .internationalUnit())
}
@Published var missingBolus: HKQuantity?
var missingBolusAmount: Double? {
missingBolus?.doubleValue(for: .internationalUnit())
}
@Published var recommendedBolus: HKQuantity?
var recommendedBolusAmount: Double? {
recommendedBolus?.doubleValue(for: .internationalUnit())
Expand Down Expand Up @@ -452,6 +456,12 @@ final class BolusEntryViewModel: ObservableObject {
formatter.numberFormatter.roundingMode = .down
return formatter.numberFormatter
}()

private lazy var breakdownBolusAmountFormatter: NumberFormatter = {
let formatter = QuantityFormatter(for: .internationalUnit())
formatter.numberFormatter.roundingMode = .halfUp
return formatter.numberFormatter
}()

private lazy var absorptionTimeFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
Expand Down Expand Up @@ -667,22 +677,33 @@ final class BolusEntryViewModel: ObservableObject {
let carbBolus: HKQuantity?
let correctionBolus: HKQuantity?
let recommendedBolus: HKQuantity?
let missingBolus: HKQuantity?
let notice: Notice?
do {
recommendation = try computeBolusRecommendation(from: state)

if let recommendation = recommendation {
if let carbsAmount = recommendation.carbsAmount {
carbBolus = HKQuantity(unit: .internationalUnit(), doubleValue: delegate.roundBolusVolume(units: carbsAmount))
carbBolus = HKQuantity(unit: .internationalUnit(), doubleValue: carbsAmount)
} else {
carbBolus = nil
}

if let correctionAmount = recommendation.correctionAmount {
correctionBolus = HKQuantity(unit: .internationalUnit(), doubleValue: delegate.roundBolusVolume(units: correctionAmount))
correctionBolus = HKQuantity(unit: .internationalUnit(), doubleValue: correctionAmount)
} else {
correctionBolus = nil
}

if let missingAmount = recommendation.missingAmount {
if missingAmount != 0 {
missingBolus = HKQuantity(unit: .internationalUnit(), doubleValue: missingAmount)
} else {
missingBolus = nil
}
} else {
missingBolus = nil
}

recommendedBolus = HKQuantity(unit: .internationalUnit(), doubleValue: delegate.roundBolusVolume(units: recommendation.amount))
//recommendedBolus = HKQuantity(unit: .internationalUnit(), doubleValue: recommendation.amount)
Expand All @@ -704,12 +725,14 @@ final class BolusEntryViewModel: ObservableObject {
} else {
carbBolus = nil
correctionBolus = nil
missingBolus = nil
recommendedBolus = HKQuantity(unit: .internationalUnit(), doubleValue: 0)
notice = nil
}
} catch {
carbBolus = nil
correctionBolus = nil
missingBolus = nil
recommendedBolus = nil

switch error {
Expand All @@ -728,6 +751,7 @@ final class BolusEntryViewModel: ObservableObject {
let priorRecommendedBolus = self.recommendedBolus
self.carbBolus = carbBolus
self.correctionBolus = correctionBolus
self.missingBolus = missingBolus
self.recommendedBolus = recommendedBolus
self.dosingDecision.manualBolusRecommendation = recommendation.map { ManualBolusRecommendationWithDate(recommendation: $0, date: now) }
self.activeNotice = notice
Expand Down Expand Up @@ -817,25 +841,32 @@ final class BolusEntryViewModel: ObservableObject {
chartDateInterval = DateInterval(start: chartStartDate, duration: .hours(totalHours))
}

func formatBolusAmount(_ bolusAmount: Double) -> String {
bolusAmountFormatter.string(from: bolusAmount) ?? String(bolusAmount)
func formatBolusAmount(_ bolusAmount: Double, forBreakdown: Bool = false) -> String {
let formatter = forBreakdown ? breakdownBolusAmountFormatter : bolusAmountFormatter
return formatter.string(from: bolusAmount) ?? String(bolusAmount)
}

var carbBolusString: String {
return bolusString(carbBolusAmount)
return bolusString(carbBolusAmount, forBreakdown: true)
}
var correctionBolusString: String {
return bolusString(correctionBolusAmount)
return bolusString(correctionBolusAmount, forBreakdown: true)
}
var negativeMissingBolusString: String {
guard missingBolusAmount != nil else {
return bolusString(nil, forBreakdown: true)
}
return bolusString(-missingBolusAmount!, forBreakdown: true)
}
var recommendedBolusString: String {
return bolusString(recommendedBolusAmount)
return bolusString(recommendedBolusAmount, forBreakdown: false)
}

func bolusString(_ bolusAmount: Double?) -> String {
func bolusString(_ bolusAmount: Double?, forBreakdown: Bool) -> String {
guard let amount = bolusAmount else {
return "–"
}
return formatBolusAmount(amount)
return formatBolusAmount(amount, forBreakdown: forBreakdown)
}

func updateEnteredBolus(_ enteredBolusString: String) {
Expand Down
Loading