Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b0ed4a3
Configurable minimum bg guard.
ps2 Jan 2, 2017
b8c34d8
Fix display issues, and clear out stored value when user deletes entr…
ps2 Jan 2, 2017
f6366c2
Return recommendation structure
ps2 Jan 2, 2017
8879699
Adding more context to bolus screen
ps2 Jan 3, 2017
7e1878a
Update bolus notices.
ps2 Jan 3, 2017
2d3d80a
another rev of the bolus ui, showing eventualbg, iob, pending insulin…
ps2 Jan 8, 2017
160e55c
Update tests
ps2 Jan 8, 2017
291e39d
Update tests
ps2 Jan 8, 2017
78b9bae
Configurable minimum bg guard.
ps2 Jan 2, 2017
09202d7
Fix display issues, and clear out stored value when user deletes entr…
ps2 Jan 2, 2017
9326033
Return recommendation structure
ps2 Jan 2, 2017
300d8c9
Adding more context to bolus screen
ps2 Jan 3, 2017
bdc7bd5
Update bolus notices.
ps2 Jan 3, 2017
29ef816
another rev of the bolus ui, showing eventualbg, iob, pending insulin…
ps2 Jan 8, 2017
920beb8
Update tests
ps2 Jan 8, 2017
fc16ca2
Update tests
ps2 Jan 8, 2017
72bab42
DoseMath.recommendBolusFromPredictedGlucose doesn't take lastTempBasal
ps2 Jan 8, 2017
5ed8473
merge
ps2 Jan 8, 2017
36975c1
merge in dev
ps2 Jan 8, 2017
2edfb70
Add HKUnit to DoseMathTests
ps2 Jan 8, 2017
43ddd5f
Move BolusRecommendation notice to enum
ps2 Jan 8, 2017
8e8dcd5
Revert defaultAbsorptionTimes
ps2 Jan 8, 2017
1fe7520
Avoid rounding above pump delivery resolution for small boluses
ps2 Jan 8, 2017
beed2ac
Assume the entirety of BolusViewController is not thread-safe, and do…
ps2 Jan 8, 2017
30acbe8
Make loopManager.getPendingInsulin private, and pass pending insulin …
ps2 Jan 8, 2017
9560db6
Use NumberFormatter.glucoseFormatter for glucose display when setting…
ps2 Jan 8, 2017
662eea9
Use DateComponentsFormatter for formatting age in minutes
ps2 Jan 8, 2017
49fa0a9
Use if case for single enum comparisons
ps2 Jan 9, 2017
7b01253
Avoid reloading tableview with static cells
ps2 Jan 12, 2017
7b72b56
Add new method for displaying glucose with units.
ps2 Feb 11, 2017
6ec4f65
Add warning symbol
ps2 Feb 11, 2017
63e0e0a
Warning symbol was showing unecessarily in some cases
ps2 Feb 11, 2017
87e78b3
Merge in dev
ps2 Feb 13, 2017
19f9a4b
Update minimumBGGuard for new settings layout
ps2 Feb 13, 2017
41ed12e
remove debug print
ps2 Feb 13, 2017
59ac40d
Remove unnecessary dispatches
ps2 Feb 13, 2017
710b75a
Let date components formatter add units string.
ps2 Feb 13, 2017
035d964
Unify how notice label is set
ps2 Feb 13, 2017
97629cd
Add resizing constraints to notice label
ps2 Feb 14, 2017
1acd876
Store age errors as dates for more accurate error messaging
ps2 Feb 19, 2017
4a3831d
Merge in dev
ps2 Feb 19, 2017
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
another rev of the bolus ui, showing eventualbg, iob, pending insulin…
…, cob, and more succinct warnings
  • Loading branch information
ps2 committed Jan 8, 2017
commit 29ef8163a6265671a833449cf62f2b82e477917f
126 changes: 43 additions & 83 deletions Loop/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1003,8 +1003,8 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
insulinSensitivitySchedule: insulinSensitivitySchedule
)

carbStore = CarbStore(
defaultAbsorptionTimes: (fast: TimeInterval(hours: 2), medium: TimeInterval(hours: 3), slow: TimeInterval(hours: 4)),
carbStore = CarbStore(
defaultAbsorptionTimes: (fast: TimeInterval(hours: 1), medium: TimeInterval(hours: 2), slow: TimeInterval(hours: 4)),
carbRatioSchedule: carbRatioSchedule,
insulinSensitivitySchedule: insulinSensitivitySchedule
)
Expand Down
41 changes: 9 additions & 32 deletions Loop/Managers/DoseMath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,11 @@ struct DoseMath {

- parameter glucose: The ascending timeline of predicted glucose values
- parameter date: The date at which the bolus would apply. Defaults to the current date.
- parameter lastTempBasal: The last-set temporary basal
- parameter maxBolus: The maximum bolus, used to constrain the output
- parameter glucoseTargetRange: The schedule of target glucose ranges
- parameter insulinSensitivity: The schedule of insulin sensitivities, in Units of insulin per glucose-unit
- parameter basalRateSchedule: The schedule of basal rates
- parameter pendingBolusAmount: The amount of insulin in any issued, but not confirmed, boluses
- parameter pendingInsulin: The amount of insulin in any issued, but not confirmed, boluses and the amount remaining from current tempBasal
- parameter minimumBGGuard: If minBG is less than or equal to this value, no recommendation will be made

- returns: The recommended bolus
Expand All @@ -152,7 +151,7 @@ struct DoseMath {
glucoseTargetRange: GlucoseRangeSchedule,
insulinSensitivity: InsulinSensitivitySchedule,
basalRateSchedule: BasalRateSchedule,
pendingBolusAmount: Double,
pendingInsulin: Double,
minimumBGGuard: GlucoseThreshold
) -> BolusRecommendation {
guard glucose.count > 1 else {
Expand All @@ -165,65 +164,43 @@ struct DoseMath {
let eventualGlucoseTargets = glucoseTargetRange.value(at: eventualGlucose.startDate)

guard minGlucose.quantity >= minimumBGGuard.quantity else {
let notice = NSLocalizedString("Glucose is predicted to go below your minimum BG Guard setting. No bolus is recommended.", comment: "Bolus recommendation message when BG is below minimum BG guard.")
return BolusRecommendation(amount: 0, notice: notice)
let notice = NSLocalizedString("Predicted glucose is below your minimum BG Guard setting.", comment: "Notice message when recommending bolus when BG is below minimum BG guard.")
return BolusRecommendation(amount: 0, pendingInsulin: pendingInsulin, notice: notice)
}

let targetGlucose = eventualGlucoseTargets.maxValue
let currentSensitivity = insulinSensitivity.quantity(at: date).doubleValue(for: glucoseTargetRange.unit)

let doseUnits = (eventualGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) - targetGlucose) / currentSensitivity

let pendingTempBasalInsulin: Double

if let lastTempBasal = lastTempBasal, lastTempBasal.unit == .unitsPerHour && lastTempBasal.endDate > date {
let normalBasalRate = basalRateSchedule.value(at: date)
let remainingTime = lastTempBasal.endDate.timeIntervalSince(date)
let remainingUnits = (lastTempBasal.value - normalBasalRate) * remainingTime / TimeInterval(hours: 1)

pendingTempBasalInsulin = max(0, remainingUnits)
} else {
pendingTempBasalInsulin = 0
}

// All outstanding potential insulin delivery
let pendingInsulin = pendingTempBasalInsulin + pendingBolusAmount

// Round to pump accuracy increments
let roundedAmount = round(max(0, (doseUnits - pendingInsulin)) * 40) / 40

// Cap at max bolus amount
let cappedAmount = min(maxBolus, max(0, roundedAmount))

let numberFormatter = NumberFormatter.glucoseFormatter(for: glucoseTargetRange.unit)
let eventualBGValueStr = numberFormatter.string(from: NSNumber(value: eventualGlucose.quantity.doubleValue(for: glucoseTargetRange.unit)))!
let eventualBG = "\(eventualBGValueStr) \(glucoseTargetRange.unit.glucoseUnitDisplayString)"


let notice: String?
if cappedAmount > 0 && minGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) < eventualGlucoseTargets.minValue {
if minGlucose.startDate == glucose[0].startDate {
notice = String(format: NSLocalizedString("Current glucose is below target range. Recommendation is based on eventual predicted BG of %3$@", comment: "Message when offering bolus prediction even though bg is below range. (1: eventual BG)"), eventualBG)
notice = NSLocalizedString("Glucose is below target range.", comment: "Message when offering bolus prediction even though bg is below range.")
} else {
let timeFormatter = DateFormatter()
timeFormatter.dateStyle = .none
timeFormatter.timeStyle = .short
let time = timeFormatter.string(from: minGlucose.startDate)

let numberFormatter = NumberFormatter.glucoseFormatter(for: glucoseTargetRange.unit)

let minBGValue = numberFormatter.string(from: NSNumber(value: minGlucose.quantity.doubleValue(for: glucoseTargetRange.unit)))!

let minBGStr = "\(minBGValue) \(glucoseTargetRange.unit.glucoseUnitDisplayString)"

notice = String(format: NSLocalizedString("Glucose is estimated to be %1$@ at %2$@. Recommendation is based on eventual predicted BG of %3$@", comment: "Message when offering bolus prediction even though bg is below range and minBG is in future. (1: glucose number)(2: glucose time)(3: eventual BG)"), minBGStr, time, eventualBG)
notice = String(format: NSLocalizedString("Predicted glucose at %1$@ is %2$@.", comment: "Message when offering bolus prediction even though bg is below range and minBG is in future. (1: glucose time)(2: glucose number)"), time, minBGStr)
}
} else if eventualGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) < eventualGlucoseTargets.minValue {

let bgStr = "\(eventualBG) \(glucoseTargetRange.unit.glucoseUnitDisplayString)"
notice = String(format: NSLocalizedString("Eventual glucose is %1$@. No bolus recommended.", comment: "Notice when eventual glucose is below range, so no bolus is recommended. (1: eventual glucose number)"), bgStr)
} else {
notice = nil
}

return BolusRecommendation(amount: cappedAmount, notice: notice)
return BolusRecommendation(amount: cappedAmount, pendingInsulin: pendingInsulin, notice: notice)
}
}
81 changes: 59 additions & 22 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,31 @@ final class LoopDataManager {
}
}

func getPendingInsulin() throws -> Double {

guard let basalRates = deviceDataManager.basalRateSchedule else {
throw LoopError.configurationError
}

let pendingTempBasalInsulin: Double
let date = Date()

if let lastTempBasal = lastTempBasal, lastTempBasal.unit == .unitsPerHour && lastTempBasal.endDate > date {
let normalBasalRate = basalRates.value(at: date)
let remainingTime = lastTempBasal.endDate.timeIntervalSince(date)
let remainingUnits = (lastTempBasal.value - normalBasalRate) * remainingTime / TimeInterval(hours: 1)

pendingTempBasalInsulin = max(0, remainingUnits)
} else {
pendingTempBasalInsulin = 0
}

let pendingBolusAmount: Double = lastBolus?.units ?? 0

// All outstanding potential insulin delivery
return pendingTempBasalInsulin + pendingBolusAmount
}

func modelPredictedGlucose(using inputs: [PredictionInputEffect], resultsHandler: @escaping (_ predictedGlucose: [GlucoseValue]?, _ error: Error?) -> Void) {
dataAccessQueue.async {
guard let
Expand Down Expand Up @@ -479,21 +504,32 @@ final class LoopDataManager {
*/
private func updatePredictedGlucoseAndRecommendedBasal() throws {
guard let
glucose = self.deviceDataManager.glucoseStore?.latestGlucose,
let pumpStatusDate = self.deviceDataManager.doseStore.lastReservoirValue?.startDate
glucose = self.deviceDataManager.glucoseStore?.latestGlucose
else {
self.predictedGlucose = nil
throw LoopError.missingDataError("Glucose")
}

guard let
pumpStatusDate = self.deviceDataManager.doseStore.lastReservoirValue?.startDate
else {
self.predictedGlucose = nil
throw LoopError.missingDataError("Cannot predict glucose due to missing input data")
throw LoopError.missingDataError("Reservoir")
}

let startDate = Date()
let recencyInterval = TimeInterval(minutes: 15)

guard startDate.timeIntervalSince(glucose.startDate) <= recencyInterval &&
startDate.timeIntervalSince(pumpStatusDate) <= recencyInterval
guard startDate.timeIntervalSince(glucose.startDate) <= recencyInterval
else {
self.predictedGlucose = nil
throw LoopError.staleDataError("Glucose Date: \(glucose.startDate) or Pump status date: \(pumpStatusDate) older than \(recencyInterval.minutes) min")
throw LoopError.glucoseTooOld(startDate.timeIntervalSince(glucose.startDate))
}

guard startDate.timeIntervalSince(pumpStatusDate) <= recencyInterval
else {
self.predictedGlucose = nil
throw LoopError.pumpDataTooOld(startDate.timeIntervalSince(pumpStatusDate))
}

guard let
Expand All @@ -502,7 +538,7 @@ final class LoopDataManager {
let insulinEffect = self.insulinEffect else
{
self.predictedGlucose = nil
throw LoopError.missingDataError("Cannot predict glucose due to missing effect data")
throw LoopError.missingDataError("Glucose effects")
}

var error: Error?
Expand Down Expand Up @@ -548,7 +584,7 @@ final class LoopDataManager {
let basalRates = deviceDataManager.basalRateSchedule,
let minimumBGGuard = deviceDataManager.minimumBGGuard
else {
error = LoopError.missingDataError("Loop configuration data not set")
error = LoopError.configurationError
throw error!
}

Expand Down Expand Up @@ -592,7 +628,7 @@ final class LoopDataManager {
}
}
} else {
resultsHandler(nil, LoopError.missingDataError("CarbStore not configured"))
resultsHandler(nil, LoopError.configurationError)
}
}

Expand All @@ -605,7 +641,7 @@ final class LoopDataManager {
let basalRates = self.deviceDataManager.basalRateSchedule,
let minimumBGGuard = self.deviceDataManager.minimumBGGuard
else {
throw LoopError.missingDataError("Bolus prediction and configuration data not found")
throw LoopError.configurationError
}

let recencyInterval = TimeInterval(minutes: 15)
Expand All @@ -615,19 +651,20 @@ final class LoopDataManager {
}

guard abs(predictedInterval) <= recencyInterval else {
throw LoopError.staleDataError("Glucose is \(predictedInterval.minutes) min old")
throw LoopError.glucoseTooOld(predictedInterval)
}

let pendingBolusAmount: Double = lastBolus?.units ?? 0

let bolusRecommendation = DoseMath.recommendBolusFromPredictedGlucose(glucose,
lastTempBasal: self.lastTempBasal,
maxBolus: maxBolus,
glucoseTargetRange: glucoseTargetRange,
insulinSensitivity: insulinSensitivity,
basalRateSchedule: basalRates,
pendingBolusAmount: pendingBolusAmount,
minimumBGGuard: minimumBGGuard)
let pendingInsulin = try self.getPendingInsulin()

let bolusRecommendation = DoseMath.recommendBolusFromPredictedGlucose(
glucose,
lastTempBasal: self.lastTempBasal,
maxBolus: maxBolus,
glucoseTargetRange: glucoseTargetRange,
insulinSensitivity: insulinSensitivity,
basalRateSchedule: basalRates,
pendingInsulin: pendingInsulin,
minimumBGGuard: minimumBGGuard)

return bolusRecommendation
}
Expand All @@ -650,7 +687,7 @@ final class LoopDataManager {
}

guard recommendedTempBasal.recommendedDate.timeIntervalSinceNow < TimeInterval(minutes: 5) else {
resultsHandler(false, LoopError.staleDataError("Recommended temp basal is \(recommendedTempBasal.recommendedDate.timeIntervalSinceNow.minutes) min old"))
resultsHandler(false, LoopError.recommendationExpired(recommendedTempBasal.recommendedDate.timeIntervalSinceNow))
return
}

Expand Down
2 changes: 1 addition & 1 deletion Loop/Models/BolusRecommendation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct BolusRecommendation {
let amount: Double
let notice: String?

init(amount: Double, notice: String? = nil) {
init(amount: Double, pendingInsulin: Double? = nil, notice: String? = nil) {
self.amount = amount
self.notice = notice
}
Expand Down
42 changes: 40 additions & 2 deletions Loop/Models/LoopError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2016 Nathan Racklyeft. All rights reserved.
//

import Foundation

enum LoopError: Error {
// Failure during device communication
Expand All @@ -20,6 +21,43 @@ enum LoopError: Error {
// Missing required data to perform an action
case missingDataError(String)

// Out-of-date required data to perform an action
case staleDataError(String)
// Glucose data is too old to perform action
case glucoseTooOld(TimeInterval)

// Pump data is too old to perform action
case pumpDataTooOld(TimeInterval)

// Recommendation Expired
case recommendationExpired(TimeInterval)
}

extension LoopError: LocalizedError {

public var errorDescription: String? {

let integerFormatter = NumberFormatter()
integerFormatter.numberStyle = .none
integerFormatter.maximumFractionDigits = 0

switch self {
case .communicationError:
return NSLocalizedString("Communication Error", comment: "The error message displayed after a communication error.")
case .configurationError:
return NSLocalizedString("Configuration Error", comment: "The error message displayed for configuration errors.")
case .connectionError:
return NSLocalizedString("No connected devices, or failure during device connection", comment: "The error message displayed for device connection errors.")
case .missingDataError(let details):
return String(format: NSLocalizedString("Missing data: %1$@", comment: "The error message for missing data. (1: missing data details)"), details)
case .glucoseTooOld(let age):
let minutes = integerFormatter.string(from: NSNumber(value: age.minutes)) ?? "??"
return String(format: NSLocalizedString("Glucose data is %1$@ minutes old", comment: "The error message when glucose data is too old to be used. (1: glucose data age in minutes)"), minutes)
case .pumpDataTooOld(let age):
let minutes = integerFormatter.string(from: NSNumber(value: age.minutes)) ?? "??"
return String(format: NSLocalizedString("Pump data is %1$@ minutes old", comment: "The error message when pump data is too old to be used. (1: pump data age in minutes)"), minutes)
case .recommendationExpired(let age):
let minutes = integerFormatter.string(from: NSNumber(value: age.minutes)) ?? "??"
return String(format: NSLocalizedString("Recommendation expired: %1$@ minutes old", comment: "The error message when a recommendation has expired. (1: age of recommendation in minutes)"), minutes)
}
}
}

Loading