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
4 changes: 4 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@
7D7076631FE06EE4004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076651FE06EE4004AC8EA /* Localizable.strings */; };
7D7076681FE0702F004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70766A1FE0702F004AC8EA /* InfoPlist.strings */; };
894F71E21FFEC4D8007D365C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 894F71E11FFEC4D8007D365C /* Assets.xcassets */; };
9E575201217D87E7002D167B /* IntegralRetrospectiveCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E575200217D87E7002D167B /* IntegralRetrospectiveCorrection.swift */; };
C10428971D17BAD400DD539A /* NightscoutUploadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */; };
C10B28461EA9BA5E006EA1FC /* far_future_high_bg_forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */; };
C11C87DE1E21EAAD00BB71D3 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; };
Expand Down Expand Up @@ -824,6 +825,7 @@
7DD382781F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainInterface.strings; sourceTree = "<group>"; };
7DD382791F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Interface.strings; sourceTree = "<group>"; };
894F71E11FFEC4D8007D365C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9E575200217D87E7002D167B /* IntegralRetrospectiveCorrection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegralRetrospectiveCorrection.swift; sourceTree = "<group>"; };
C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NightscoutUploadKit.framework; path = Carthage/Build/iOS/NightscoutUploadKit.framework; sourceTree = SOURCE_ROOT; };
C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = far_future_high_bg_forecast.json; sourceTree = "<group>"; };
C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_very_low_end_in_range.json; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1195,6 +1197,7 @@
430C1ABC1E5568A80067F1AE /* StatusChartsManager+LoopKit.swift */,
4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */,
4328E0341CFC0AE100E199AA /* WatchDataManager.swift */,
9E575200217D87E7002D167B /* IntegralRetrospectiveCorrection.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1868,6 +1871,7 @@
43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */,
43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */,
4372E48B213CB5F00068E043 /* Double.swift in Sources */,
9E575201217D87E7002D167B /* IntegralRetrospectiveCorrection.swift in Sources */,
430B29932041F5B300BA9F93 /* UserDefaults+Loop.swift in Sources */,
4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */,
43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */,
Expand Down
161 changes: 161 additions & 0 deletions Loop/Managers/IntegralRetrospectiveCorrection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// IntegralRetrospectiveCorrection.swift
// Loop
//
// Created by Dragan Maksimovic on 10/21/18.
// Copyright © 2018 LoopKit Authors. All rights reserved.
//

import Foundation
import HealthKit
import LoopKit

/**
Integral Retrospective Correction (IRC) calculates a correction effect in glucose prediction based on a timeline of past discrepancies between observed glucose movement and movement expected based on insulin and carb models. Integral retrospective correction acts as a proportional-integral-differential (PID) 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), whereas "past discrepancies" refers to a timeline of discrepancies computed over retrospective correction integration interval (set to 180 min in Loop Settings).

*/
class IntegralRetrospectiveCorrection {

/**
Integral retrospective correction parameters:
- currentDiscrepancyGain: Standard retrospective correction gain
- persistentDiscrepancyGain: Gain for persistent long-term modeling errors, must be greater than or equal to currentDiscrepancyGain
- correctionTimeConstant: How fast integral effect accumulates in response to persistent errors
- delta: Glucose sampling time interval (5 min)
- differentialGain: Differential effect gain
- maximumCorrectionEffectDuration: Maximum duration of the correction effect in glucose prediction
*/
static let currentDiscrepancyGain: Double = 1.0
static let persistentDiscrepancyGain: Double = 5.0
static let correctionTimeConstant: TimeInterval = TimeInterval(minutes: 120.0)
static let differentialGain: Double = 2.0
static let delta: TimeInterval = TimeInterval(minutes: 5.0)
static let maximumCorrectionEffectDuration: TimeInterval = TimeInterval(minutes: 240.0)

/// Initialize computed integral retrospective correction parameters
static let integralForget: Double = exp( -delta.minutes / correctionTimeConstant.minutes )
static let integralGain: Double = ((1 - integralForget) / integralForget) *
(persistentDiscrepancyGain - currentDiscrepancyGain)
static let proportionalGain: Double = currentDiscrepancyGain - integralGain

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

/// Effect duration for standard retrospective correction
private let effectDuration: TimeInterval

/// Settings relevant for calculation of effect limits
private let settings: LoopSettings
private let correctionRange: GlucoseRangeSchedule
private let insulinSensitivity: InsulinSensitivitySchedule
private let basalRates: BasalRateSchedule

/**
Initialize integral retrospective correction settings based on current values of user settings

- Parameters:
- effectDuration: Effect duration for standard retrospective correction
- settings: User Loop settings
- correctionRange: User correction range settings
- insulinSensitivity: User insulin sensitivity schedule
- basalRates: User basal rate schedule

- Returns: Integral Retrospective Correction customized with controller parameters and user settings
*/
init(_ effectDuration: TimeInterval, _ settings: LoopSettings, _ correctionRange: GlucoseRangeSchedule, _ insulinSensitivity: InsulinSensitivitySchedule, _ basalRates: BasalRateSchedule) {

self.effectDuration = effectDuration
self.settings = settings
self.correctionRange = correctionRange
self.insulinSensitivity = insulinSensitivity
self.basalRates = basalRates
}

/**
Calculate correction effect and correction effect duration based on timeline of past discrepancies

- Parameters:
- currentDate: Date when timeline of past discrepancies is computed
- currentDiscrepancy: Most recent discrepancy
- latestGlucose: Most recent glucose
- pastDiscrepancies: Timeline of past discepancies

- Returns:
- totalRetrospectiveCorrection: Overall glucose effect
- integralCorrectionEffectDuration: Effect duration

*/
func updateIntegralRetrospectiveCorrection(_ currentDate: Date,
_ currentDiscrepancy: GlucoseChange, _ latestGlucose: GlucoseValue,
_ pastDiscrepancies: [GlucoseChange]) -> (HKQuantity, TimeInterval) {

/// To reduce response delay, integral retrospective correction is computed over an array of recent contiguous discrepancy values having the same sign as the most recent discrepancy value
var recentDiscrepancyValues: [Double] = []
var nextDiscrepancy = currentDiscrepancy
let currentDiscrepancySign = currentDiscrepancy.quantity.doubleValue(for: unit).sign
for pastDiscrepancy in pastDiscrepancies.reversed() {
let pastDiscrepancyValue = pastDiscrepancy.quantity.doubleValue(for: unit)
if (pastDiscrepancyValue.sign == currentDiscrepancySign &&
nextDiscrepancy.endDate.timeIntervalSince(pastDiscrepancy.endDate)
<= settings.recencyInterval &&
abs(pastDiscrepancyValue) >= 0.1)
{
recentDiscrepancyValues.append(pastDiscrepancyValue)
nextDiscrepancy = pastDiscrepancy
} else {
break
}
}
recentDiscrepancyValues = recentDiscrepancyValues.reversed()

/// User settings relevant for calculations of effect limits
let currentSensitivity = insulinSensitivity.quantity(at: currentDate).doubleValue(for: unit)
let currentBasalRate = basalRates.value(at: currentDate)
let correctionRangeMin = correctionRange.minQuantity(at: currentDate).doubleValue(for: unit)
let correctionRangeMax = correctionRange.maxQuantity(at: currentDate).doubleValue(for: unit)

let latestGlucoseValue = latestGlucose.quantity.doubleValue(for: unit) // most recent glucose

/// Safety limit for (+) integral effect. The limit is set to a larger value if the current blood glucose is further away from the correction range because we have more time available for corrections
let glucoseError = latestGlucoseValue - correctionRangeMax
let zeroTempEffect = abs(currentSensitivity * currentBasalRate)
let integralEffectPositiveLimit = min(max(glucoseError, 0.5 * zeroTempEffect), 4.0 * zeroTempEffect)

/// Limit for (-) integral effect: glucose prediction reduced by no more than 10 mg/dL below the correction range minimum
let integralEffectNegativeLimit = -max(10.0, latestGlucoseValue - correctionRangeMin)

/// Integral correction math
var integralCorrection = 0.0
var integralCorrectionEffectMinutes = effectDuration.minutes - 2.0 * IntegralRetrospectiveCorrection.delta.minutes
for discrepancy in recentDiscrepancyValues {
integralCorrection =
IntegralRetrospectiveCorrection.integralForget * integralCorrection +
IntegralRetrospectiveCorrection.integralGain * discrepancy
integralCorrectionEffectMinutes += 2.0 * IntegralRetrospectiveCorrection.delta.minutes
}
/// Limits applied to integral correction effect and effect duration
integralCorrection = min(max(integralCorrection, integralEffectNegativeLimit), integralEffectPositiveLimit)
integralCorrectionEffectMinutes = min(integralCorrectionEffectMinutes, IntegralRetrospectiveCorrection.maximumCorrectionEffectDuration.minutes)

/// Differential correction math
let currentDiscrepancyValue = currentDiscrepancy.quantity.doubleValue(for: unit)
var differentialDiscrepancy: Double = 0.0
if recentDiscrepancyValues.count > 1 {
let previousDiscrepancyValue = recentDiscrepancyValues[recentDiscrepancyValues.count - 2]
differentialDiscrepancy = currentDiscrepancyValue - previousDiscrepancyValue
}

/// Overall glucose effect calculated as a sum of propotional, integral and differential correction effects
let proportionalCorrection = IntegralRetrospectiveCorrection.proportionalGain * currentDiscrepancyValue
let differentialCorrection = IntegralRetrospectiveCorrection.differentialGain * differentialDiscrepancy
let totalCorrection = proportionalCorrection + integralCorrection + differentialCorrection
let totalRetrospectiveCorrection = HKQuantity(unit: unit, doubleValue: totalCorrection)
let integralCorrectionEffectDuration = TimeInterval(minutes: integralCorrectionEffectMinutes)

/// Return overall retrospective correction effect and effect duration
return((totalRetrospectiveCorrection, integralCorrectionEffectDuration))
}

}
Loading