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
16 changes: 16 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */; };
434FB6461D68F1CD007B9C70 /* Amplitude.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 434FB6451D68F1CD007B9C70 /* Amplitude.framework */; };
434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */; };
43511CE221FD80E400566C63 /* RetrospectiveCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43511CDF21FD80E400566C63 /* RetrospectiveCorrection.swift */; };
43511CE321FD80E400566C63 /* StandardRetrospectiveCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43511CE021FD80E400566C63 /* StandardRetrospectiveCorrection.swift */; };
43511CEE220FC61700566C63 /* HUDRowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43511CED220FC61700566C63 /* HUDRowController.swift */; };
43523EDB1CC35083001850F1 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43523EDA1CC35083001850F1 /* RileyLinkKit.framework */; };
435400311C9F744E00D5819C /* BolusSuggestionUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */; };
Expand Down Expand Up @@ -617,6 +619,8 @@
434FB6451D68F1CD007B9C70 /* Amplitude.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Amplitude.framework; sourceTree = BUILT_PRODUCTS_DIR; };
434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = "<group>"; };
434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; };
43511CDF21FD80E400566C63 /* RetrospectiveCorrection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetrospectiveCorrection.swift; sourceTree = "<group>"; };
43511CE021FD80E400566C63 /* StandardRetrospectiveCorrection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardRetrospectiveCorrection.swift; sourceTree = "<group>"; };
43511CED220FC61700566C63 /* HUDRowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDRowController.swift; sourceTree = "<group>"; };
43523EDA1CC35083001850F1 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusSuggestionUserInfo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1202,9 +1206,19 @@
path = Display;
sourceTree = "<group>";
};
43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */ = {
isa = PBXGroup;
children = (
43511CDF21FD80E400566C63 /* RetrospectiveCorrection.swift */,
43511CE021FD80E400566C63 /* StandardRetrospectiveCorrection.swift */,
);
path = RetrospectiveCorrection;
sourceTree = "<group>";
};
43757D131C06F26C00910CB9 /* Models */ = {
isa = PBXGroup;
children = (
43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */,
43880F961D9D8052009061A8 /* ServiceAuthentication */,
C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */,
43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */,
Expand Down Expand Up @@ -2458,6 +2472,7 @@
43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */,
C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */,
43FCEEA9221A615B0013DD30 /* StatusChartsManager.swift in Sources */,
43511CE321FD80E400566C63 /* StandardRetrospectiveCorrection.swift in Sources */,
435400321C9F745500D5819C /* BolusSuggestionUserInfo.swift in Sources */,
43E3449F1B9D68E900C85C07 /* StatusTableViewController.swift in Sources */,
43DBF0531C93EC8200B3C386 /* DeviceDataManager.swift in Sources */,
Expand Down Expand Up @@ -2518,6 +2533,7 @@
4372E490213CFCE70068E043 /* LoopSettingsUserInfo.swift in Sources */,
89CA2B3D226E6B13004D9350 /* LocalTestingScenariosManager.swift in Sources */,
43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */,
43511CE221FD80E400566C63 /* RetrospectiveCorrection.swift in Sources */,
438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */,
892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */,
4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */,
Expand Down
54 changes: 34 additions & 20 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ final class LoopDataManager {

glucoseStore = GlucoseStore(healthStore: healthStore, cacheStore: cacheStore, cacheLength: .hours(24))

retrospectiveCorrection = settings.enabledRetrospectiveCorrectionAlgorithm

overrideHistory.delegate = self
cacheStore.delegate = self

Expand Down Expand Up @@ -218,6 +220,9 @@ final class LoopDataManager {
}
}

// Confined to dataAccessQueue
private var retrospectiveCorrection: RetrospectiveCorrection

// MARK: - Background task management

private var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
Expand Down Expand Up @@ -647,7 +652,7 @@ extension LoopDataManager {
throw LoopError.missingDataError(.glucose)
}

let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-settings.retrospectiveCorrectionIntegrationInterval)
let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-retrospectiveCorrection.retrospectionInterval)

let earliestEffectDate = Date(timeIntervalSinceNow: .hours(-24))
let nextEffectDate = insulinCounteractionEffects.last?.endDate ?? earliestEffectDate
Expand Down Expand Up @@ -827,39 +832,38 @@ extension LoopDataManager {
return prediction
}

/// Generates an effect based on how large the discrepancy is between the current glucose and its predicted value.
/// Generates a correction effect based on how large the discrepancy is between the current glucose and its model predicted value.
///
/// - Parameter effectDuration: The length of time to extend the effect
/// - Throws: LoopError.missingDataError
private func updateRetrospectiveGlucoseEffect(effectDuration: TimeInterval = TimeInterval(minutes: 60)) throws {
private func updateRetrospectiveGlucoseEffect() throws {
dispatchPrecondition(condition: .onQueue(dataAccessQueue))

// Get carb effects, otherwise clear effect and throw error
guard let carbEffects = self.carbEffect else {
retrospectiveGlucoseDiscrepancies = nil
retrospectiveGlucoseEffect = []
throw LoopError.missingDataError(.carbEffect)
}

retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta)

// Our last change should be recent, otherwise clear the effects
guard let discrepancy = retrospectiveGlucoseDiscrepanciesSummed?.last,
Date().timeIntervalSince(discrepancy.endDate) <= settings.recencyInterval
else {
retrospectiveGlucoseEffect = []
return
}

// Get most recent glucose, otherwise clear effect and throw error
guard let glucose = self.glucoseStore.latestGlucose else {
retrospectiveGlucoseEffect = []
throw LoopError.missingDataError(.glucose)
}

let unit = HKUnit.milligramsPerDeciliter
let discrepancyTime = max(discrepancy.endDate.timeIntervalSince(discrepancy.startDate), settings.retrospectiveCorrectionGroupingInterval)
let velocity = HKQuantity(unit: unit.unitDivided(by: .second()), doubleValue: discrepancy.quantity.doubleValue(for: unit) / discrepancyTime)
// Get timeline of glucose discrepancies
retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta)

retrospectiveGlucoseEffect = glucose.decayEffect(atRate: velocity, for: effectDuration)
// Calculate retrospective correction
retrospectiveGlucoseEffect = retrospectiveCorrection.computeEffect(
startingAt: glucose,
retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
recencyInterval: settings.recencyInterval,
insulinSensitivitySchedule: insulinSensitivitySchedule,
basalRateSchedule: basalRateSchedule,
glucoseCorrectionRangeSchedule: settings.glucoseTargetRangeSchedule,
retrospectiveCorrectionGroupingInterval: settings.retrospectiveCorrectionGroupingInterval
)
}

/// Runs the glucose prediction on the latest effect data.
Expand Down Expand Up @@ -1026,6 +1030,9 @@ protocol LoopState {
/// The difference in predicted vs actual glucose over a recent period
var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? { get }

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

/// Calculates a new prediction from the current data using the specified effect inputs
///
/// This method is intended for visualization purposes only, not dosing calculation. No validation of input data is done.
Expand Down Expand Up @@ -1087,6 +1094,11 @@ extension LoopDataManager {
return loopDataManager.retrospectiveGlucoseDiscrepanciesSummed
}

var totalRetrospectiveCorrection: HKQuantity? {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
return loopDataManager.retrospectiveCorrection.totalGlucoseCorrectionEffect
}

func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] {
return try loopDataManager.predictGlucose(using: inputs)
}
Expand Down Expand Up @@ -1158,7 +1170,7 @@ extension LoopDataManager {

"retrospectiveGlucoseDiscrepancies: [",
"* GlucoseEffect(start, mg/dL)",
(manager.retrospectiveGlucoseDiscrepancies ?? []).reduce(into: "", { (entries, entry) in
(state.retrospectiveGlucoseDiscrepancies ?? []).reduce(into: "", { (entries, entry) in
entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n")
}),
"]",
Expand All @@ -1182,6 +1194,8 @@ extension LoopDataManager {
"",
"cacheStore: \(String(reflecting: self.glucoseStore.cacheStore))",
"",
String(reflecting: self.retrospectiveCorrection),
"",
]

self.glucoseStore.generateDiagnosticReport { (report) in
Expand All @@ -1206,7 +1220,7 @@ extension LoopDataManager {


extension Notification.Name {
static let LoopDataUpdated = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopDataUpdated")
static let LoopDataUpdated = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopDataUpdated")

static let LoopRunning = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopRunning")
}
Expand Down
8 changes: 8 additions & 0 deletions Loop/Models/LoopSettings+Loop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Copyright © 2018 LoopKit Authors. All rights reserved.
//

import Foundation
import LoopCore

// MARK: - Static configuration
Expand All @@ -16,4 +17,11 @@ extension LoopSettings {
}
return inputs
}

static let retrospectiveCorrectionEffectDuration = TimeInterval(hours: 1)

/// Creates an instance of the enabled retrospective correction implementation
var enabledRetrospectiveCorrectionAlgorithm: RetrospectiveCorrection {
return StandardRetrospectiveCorrection(effectDuration: LoopSettings.retrospectiveCorrectionEffectDuration)
}
}
36 changes: 36 additions & 0 deletions Loop/Models/RetrospectiveCorrection/RetrospectiveCorrection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// RetrospectiveCorrection.swift
// Loop
//
// Copyright © 2019 LoopKit Authors. All rights reserved.
//

import Foundation
import HealthKit
import LoopKit


/// Derives a continued glucose effect from recent prediction discrepancies
protocol RetrospectiveCorrection: CustomDebugStringConvertible {
/// The maximum interval of historical glucose discrepancies that should be provided to the computation
var retrospectionInterval: TimeInterval { get }

/// Overall retrospective correction effect
var totalGlucoseCorrectionEffect: HKQuantity? { get }

/// Calculates overall correction effect based on timeline of discrepancies, and updates glucoseCorrectionEffect
///
/// - Parameters:
/// - glucose: Most recent glucose
/// - retrospectiveGlucoseDiscrepanciesSummed: Timeline of past discepancies
/// - Returns: Glucose correction effects
func computeEffect(
startingAt startingGlucose: GlucoseValue,
retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?,
recencyInterval: TimeInterval,
insulinSensitivitySchedule: InsulinSensitivitySchedule?,
basalRateSchedule: BasalRateSchedule?,
glucoseCorrectionRangeSchedule: GlucoseRangeSchedule?,
retrospectiveCorrectionGroupingInterval: TimeInterval
) -> [GlucoseEffect]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// StandardRetrospectiveCorrection.swift
// Loop
//
// Created by Dragan Maksimovic on 10/27/18.
// Copyright © 2018 LoopKit Authors. All rights reserved.
//

import Foundation
import HealthKit
import LoopKit

/**
Standard Retrospective Correction (RC) calculates a correction effect in glucose prediction based on the most recent discrepancy between observed glucose movement and movement expected based on insulin and carb models. Standard retrospective correction acts as a proportional (P) controller aimed at reducing modeling errors in glucose prediction.

In the above summary, "discrepancy" is a difference between the actual glucose and the model predicted glucose over retrospective correction grouping interval (set to 30 min in LoopSettings)
*/
class StandardRetrospectiveCorrection: RetrospectiveCorrection {
let retrospectionInterval = TimeInterval(minutes: 30)

/// RetrospectiveCorrection protocol variables
/// Standard effect duration
let effectDuration: TimeInterval
/// Overall retrospective correction effect
var totalGlucoseCorrectionEffect: HKQuantity?

/// All math is performed with glucose expressed in mg/dL
private let unit = HKUnit.milligramsPerDeciliter

init(effectDuration: TimeInterval) {
self.effectDuration = effectDuration
}

func computeEffect(
startingAt startingGlucose: GlucoseValue,
retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]?,
recencyInterval: TimeInterval,
insulinSensitivitySchedule: InsulinSensitivitySchedule?,
basalRateSchedule: BasalRateSchedule?,
glucoseCorrectionRangeSchedule: GlucoseRangeSchedule?,
retrospectiveCorrectionGroupingInterval: TimeInterval
) -> [GlucoseEffect] {
// Last discrepancy should be recent, otherwise clear the effect and return
let glucoseDate = startingGlucose.startDate
guard let currentDiscrepancy = retrospectiveGlucoseDiscrepanciesSummed?.last,
glucoseDate.timeIntervalSince(currentDiscrepancy.endDate) <= recencyInterval
else {
totalGlucoseCorrectionEffect = nil
return []
}

// Standard retrospective correction math
let currentDiscrepancyValue = currentDiscrepancy.quantity.doubleValue(for: unit)
totalGlucoseCorrectionEffect = HKQuantity(unit: unit, doubleValue: currentDiscrepancyValue)

let retrospectionTimeInterval = currentDiscrepancy.endDate.timeIntervalSince(currentDiscrepancy.startDate)
let discrepancyTime = max(retrospectionTimeInterval, retrospectiveCorrectionGroupingInterval)
let velocity = HKQuantity(unit: unit.unitDivided(by: .second()), doubleValue: currentDiscrepancyValue / discrepancyTime)

// Update array of glucose correction effects
return startingGlucose.decayEffect(atRate: velocity, for: effectDuration)
}

var debugDescription: String {
let report: [String] = [
"## StandardRetrospectiveCorrection",
""
]

return report.joined(separator: "\n")
}
}
Loading