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

Negative Insulin Damper #2247

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
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 @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
120490CB2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 120490CA2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift */; };
1419606428D9550400BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; };
1419606928D9554E00BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; };
1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; };
Expand Down Expand Up @@ -743,6 +744,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
120490CA2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NegativeInsulinDamperSelectionView.swift; sourceTree = "<group>"; };
142CB7582A60BF2E0075748A /* EditMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMode.swift; sourceTree = "<group>"; };
142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = "<group>"; };
1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2276,6 +2278,7 @@
C1AF062229426300002C1B19 /* ManualGlucoseEntryRow.swift */,
DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */,
DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */,
120490CA2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -3832,6 +3835,7 @@
895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */,
C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */,
A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */,
120490CB2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift in Sources */,
439706E622D2E84900C81566 /* PredictionSettingTableViewCell.swift in Sources */,
430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */,
43A51E211EB6DBDD000736CC /* LoopChartsTableViewController.swift in Sources */,
Expand Down
131 changes: 126 additions & 5 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ final class LoopDataManager {
predictedGlucose = nil
}
}

private var negativeInsulinDamper: Double? {
didSet {
predictedGlucose = nil
}
}

/// When combining retrospective glucose discrepancies, extend the window slightly as a buffer.
private let retrospectiveCorrectionGroupingIntervalMultiplier = 1.01
Expand Down Expand Up @@ -995,6 +1001,68 @@ extension LoopDataManager {
let earliestEffectDate = Date(timeInterval: .hours(-24), since: now())
let nextCounteractionEffectDate = insulinCounteractionEffects.last?.endDate ?? earliestEffectDate
let insulinEffectStartDate = nextCounteractionEffectDate.addingTimeInterval(.minutes(-5))
let updateInsulinEffectNeeded = insulinEffect == nil || insulinEffect?.first?.startDate ?? .distantFuture > insulinEffectStartDate

if negativeInsulinDamper == nil || updateInsulinEffectNeeded {
self.logger.debug("Recomputing negative insulin damper")
updateGroup.enter()
let lastDoseStartDate = nextCounteractionEffectDate.addingTimeInterval(.minutes(-15))
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: lastDoseStartDate, basalDosingEnd: lastDoseStartDate) { (result) -> Void in
switch result {
case .failure(let error):
self.logger.error("Could not fetch insulin effects for damper: %{public}@", error.localizedDescription)
self.insulinEffect = nil
warnings.append(.fetchDataWarning(.insulinEffect(error: error)))
case .success(let effects):
var posDeltaSum = 0.0
effects.enumerated().forEach{
if $0.offset > 0 {
let delta = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter) - effects[$0.offset - 1].quantity.doubleValue(for: .milligramsPerDeciliter)
posDeltaSum += max(0, delta)
}
}

let insulinSensitivity = latestSettings.insulinSensitivitySchedule!.quantity(at: lastGlucoseDate)
let basalRate = latestSettings.basalRateSchedule!.value(at: lastGlucoseDate)
let model = self.doseStore.insulinModelProvider.model(for: self.pumpInsulinType)

// anchorScale is set to 1 hour for rapid acting adult, and 44 minutes for ultra-rapid insulins
let anchorScale: Double
if let expModel = model as? ExponentialInsulinModel {
anchorScale = 0.8 * expModel.peakActivityTime.hours
} else {
anchorScale = 1.0
}

// NID will change the final prediction so that positive changes will be multiplied by weight alpha
// the long term slope will be marginalSlope
// in the initial linear scaling region alpha will be anchorAlpha at anchorPoint
// note that anchorPoint is unaffected by overrides (the changes cancel out)
let marginalSlope = 0.05
let anchorPoint = anchorScale * basalRate * insulinSensitivity.doubleValue(for: .milligramsPerDeciliter)
let anchorAlpha = 0.75

let linearScaleSlope = (1.0 - anchorAlpha)/anchorPoint // how alpha scales down in the linear scale region

// the slope in the linear scale region of alpha * posDeltaSum is 1 - 2*linearScaleSlope*posDeltaSum.
// the transitionPoint is where we transition from linear scale region to marginalSlope. The slope is continuous at this point
let transitionPoint = (1 - marginalSlope) / (2 * linearScaleSlope)

let alpha : Double
if posDeltaSum < transitionPoint { // linear scaling region
alpha = 1 - linearScaleSlope * posDeltaSum
} else { // marginal slope region
let transitionValue = (1 - linearScaleSlope * transitionPoint) * transitionPoint
alpha = (transitionValue + marginalSlope * (posDeltaSum - transitionPoint)) / posDeltaSum
}

self.negativeInsulinDamper = max(0, min(1, 1 - alpha))
}

updateGroup.leave()
}

}

if glucoseMomentumEffect == nil {
updateGroup.enter()
Expand All @@ -1011,10 +1079,10 @@ extension LoopDataManager {
}
}

if insulinEffect == nil || insulinEffect?.first?.startDate ?? .distantFuture > insulinEffectStartDate {
if updateInsulinEffectNeeded {
self.logger.debug("Recomputing insulin effects")
updateGroup.enter()
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: now()) { (result) -> Void in
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: nil, basalDosingEnd: now()) { (result) -> Void in
switch result {
case .failure(let error):
self.logger.error("Could not fetch insulin effects: %{public}@", error.localizedDescription)
Expand All @@ -1030,7 +1098,7 @@ extension LoopDataManager {

if insulinEffectIncludingPendingInsulin == nil {
updateGroup.enter()
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: nil) { (result) -> Void in
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: nil, basalDosingEnd: nil) { (result) -> Void in
switch result {
case .failure(let error):
self.logger.error("Could not fetch insulin effects including pending insulin: %{public}@", error.localizedDescription)
Expand Down Expand Up @@ -1199,7 +1267,7 @@ extension LoopDataManager {
// All outstanding potential insulin delivery
return pendingTempBasalInsulin + pendingBolusAmount
}

/// - Throws:
/// - LoopError.missingDataError
/// - LoopError.configurationError
Expand Down Expand Up @@ -1337,6 +1405,51 @@ extension LoopDataManager {
}

var prediction = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: effects)

if inputs.contains(.damper), let damper = negativeInsulinDamper {
let damperOnly = inputs.isSubset(of: [.damper])
if damperOnly {
prediction = try predictGlucose(
startingAt: startingGlucoseOverride,
using: settings.enabledEffects.subtracting(.damper),
historicalInsulinEffect: insulinEffectOverride,
insulinCounteractionEffects: insulinCounteractionEffectsOverride,
historicalCarbEffect: carbEffectOverride,
potentialBolus: potentialBolus,
potentialCarbEntry: potentialCarbEntry,
replacingCarbEntry: replacedCarbEntry,
includingPendingInsulin: includingPendingInsulin,
includingPositiveVelocityAndRC: includingPositiveVelocityAndRC)
}

let alpha = 1 - damper
var dampedPrediction = [PredictedGlucoseValue]()
var value = 0.0
prediction.enumerated().forEach{

if $0.offset == 0 {
value = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter)
dampedPrediction.append($0.element)
return
}
let delta = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter) - prediction[$0.offset - 1].quantity.doubleValue(for: .milligramsPerDeciliter)

if damperOnly {
// we just want to display the effects of damper relative to everything else
if delta > 0 {
value -= damper * delta
}
} else if delta > 0 {
value += alpha * delta
} else {
value += delta
}
dampedPrediction.append(PredictedGlucoseValue(startDate: $0.element.startDate, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value)))
}

prediction = dampedPrediction
}


// Dosing requires prediction entries at least as long as the insulin model duration.
// If our prediction is shorter than that, then extend it here.
Expand Down Expand Up @@ -1367,7 +1480,7 @@ extension LoopDataManager {
var insulinEffect: [GlucoseEffect]?
let basalDosingEnd = includingPendingInsulin ? nil : now()
updateGroup.enter()
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: basalDosingEnd) { result in
doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: nil, basalDosingEnd: basalDosingEnd) { result in
switch result {
case .failure(let error):
effectCalculationError.mutate { $0 = error }
Expand Down Expand Up @@ -1955,6 +2068,9 @@ protocol LoopState {

/// The total corrective glucose effect from retrospective correction
var totalRetrospectiveCorrection: HKQuantity? { get }

/// The negative insulin damper - if present then is in the range [0,1]
var negativeInsulinDamper: Double? { get}

/// Calculates a new prediction from the current data using the specified effect inputs
///
Expand Down Expand Up @@ -2079,6 +2195,11 @@ extension LoopDataManager {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
return loopDataManager.retrospectiveCorrection.totalGlucoseCorrectionEffect
}

var negativeInsulinDamper: Double? {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
return loopDataManager.negativeInsulinDamper
}

func predictGlucose(using inputs: PredictionInputEffect, potentialBolus: DoseEntry?, potentialCarbEntry: NewCarbEntry?, replacingCarbEntry replacedCarbEntry: StoredCarbEntry?, includingPendingInsulin: Bool, considerPositiveVelocityAndRC: Bool) throws -> [PredictedGlucoseValue] {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
Expand Down
2 changes: 1 addition & 1 deletion Loop/Managers/Store Protocols/DoseStoreProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ protocol DoseStoreProtocol: AnyObject {
// MARK: IOB and insulin effect
func insulinOnBoard(at date: Date, completion: @escaping (_ result: DoseStoreResult<InsulinValue>) -> Void)

func getGlucoseEffects(start: Date, end: Date?, basalDosingEnd: Date?, completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void)
func getGlucoseEffects(start: Date, end: Date?, doseEnd: Date?, basalDosingEnd: Date?, completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void)

func getInsulinOnBoardValues(start: Date, end: Date? , basalDosingEnd: Date?, completion: @escaping (_ result: DoseStoreResult<[InsulinValue]>) -> Void)

Expand Down
3 changes: 3 additions & 0 deletions Loop/Models/LoopSettings+Loop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ extension LoopSettings {
if !LoopConstants.retrospectiveCorrectionEnabled {
inputs.remove(.retrospection)
}
if !UserDefaults.standard.negativeInsulinDamperEnabled {
inputs.remove(.damper)
}
return inputs
}
}
7 changes: 6 additions & 1 deletion Loop/Models/PredictionInputEffect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ struct PredictionInputEffect: OptionSet {
static let momentum = PredictionInputEffect(rawValue: 1 << 2)
static let retrospection = PredictionInputEffect(rawValue: 1 << 3)
static let suspend = PredictionInputEffect(rawValue: 1 << 4)
static let damper = PredictionInputEffect(rawValue: 1 << 5)

static let all: PredictionInputEffect = [.carbs, .insulin, .momentum, .retrospection]
static let all: PredictionInputEffect = [.carbs, .insulin, .damper, .momentum, .retrospection]

var localizedTitle: String? {
switch self {
case [.carbs]:
return NSLocalizedString("Carbohydrates", comment: "Title of the prediction input effect for carbohydrates")
case [.insulin]:
return NSLocalizedString("Insulin", comment: "Title of the prediction input effect for insulin")
case [.damper]:
return NSLocalizedString("Negative Insulin Damper", comment: "Title of the prediction input effect for negative insulin damper")
case [.momentum]:
return NSLocalizedString("Glucose Momentum", comment: "Title of the prediction input effect for glucose momentum")
case [.retrospection]:
Expand All @@ -44,6 +47,8 @@ struct PredictionInputEffect: OptionSet {
return String(format: NSLocalizedString("Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for carbohydrates. (1: The glucose unit string)"), unit.localizedShortUnitString)
case [.insulin]:
return String(format: NSLocalizedString("Insulin Absorbed (U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for insulin"), unit.localizedShortUnitString)
case [.damper]:
return String(format: NSLocalizedString("Reduces increases in glucose. The damper is stronger when there is more negative insulin", comment: "Description of the prediction input effect for negative insulin damper"), unit.localizedShortUnitString)
case [.momentum]:
return NSLocalizedString("15 min glucose regression coefficient (b₁), continued with decay over 30 min", comment: "Description of the prediction input effect for glucose momentum")
case [.retrospection]:
Expand Down
Loading