has changes to apply Kalman filter on Libre, still in test

This commit is contained in:
Johan Degraeve 2020-12-29 22:13:25 +01:00
parent e1a9c04eb5
commit 8b62ecd348
13 changed files with 484 additions and 281 deletions

View File

@ -239,7 +239,7 @@
F85DC2F321CFE3D400B9F74A /* Calibration+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DC2F021CFE3D400B9F74A /* Calibration+CoreDataClass.swift */; };
F85DC2F421CFE3D400B9F74A /* Sensor+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DC2F121CFE3D400B9F74A /* Sensor+CoreDataClass.swift */; };
F85DC2F521CFE3D400B9F74A /* BgReading+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DC2F221CFE3D400B9F74A /* BgReading+CoreDataClass.swift */; };
F85FB769255DE14600D1C39E /* ConstantsSmoothing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FB768255DE14600D1C39E /* ConstantsSmoothing.swift */; };
F85FB769255DE14600D1C39E /* ConstantsLibreSmoothing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FB768255DE14600D1C39E /* ConstantsLibreSmoothing.swift */; };
F85FF39125288870004E6FF1 /* HouseKeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FF39025288870004E6FF1 /* HouseKeeper.swift */; };
F85FF3C4252D0C32004E6FF1 /* xdrip.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F85FF3C2252D0C32004E6FF1 /* xdrip.xcdatamodeld */; };
F85FF3CD252F9FD7004E6FF1 /* SnoozeParameters+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FF3CC252F9FD7004E6FF1 /* SnoozeParameters+CoreDataProperties.swift */; };
@ -376,10 +376,10 @@
F8B48A9C22B2FA66009BCC01 /* SpeakReading.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8B48A9A22B2FA66009BCC01 /* SpeakReading.strings */; };
F8B48AA022B2FA7B009BCC01 /* HomeView.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8B48A9E22B2FA7B009BCC01 /* HomeView.strings */; };
F8B48AA422B2FA9B009BCC01 /* CalibrationRequest.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8B48AA222B2FA9A009BCC01 /* CalibrationRequest.strings */; };
F8B955EB2591355200C06016 /* CGMLibre2Transmitter+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B955EA2591355200C06016 /* CGMLibre2Transmitter+TestData.swift */; };
F8B9560A259294FA00C06016 /* GlucoseDataFilterFlatValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B95609259294FA00C06016 /* GlucoseDataFilterFlatValues.swift */; };
F8B955B1258BEE9D00C06016 /* ConstantsSpeakReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B955B0258BEE9D00C06016 /* ConstantsSpeakReading.swift */; };
F8B955B7258D5E2000C06016 /* ConstantsHealthKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B955B6258D5E2000C06016 /* ConstantsHealthKit.swift */; };
F8B955EB2591355200C06016 /* CGMLibre2Transmitter+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B955EA2591355200C06016 /* CGMLibre2Transmitter+TestData.swift */; };
F8B9560A259294FA00C06016 /* GlucoseDataFilterFlatValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B95609259294FA00C06016 /* GlucoseDataFilterFlatValues.swift */; };
F8BDD4242218790E006EAB84 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD4232218790E006EAB84 /* UserDefaults.swift */; };
F8BDD438221A0349006EAB84 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8BDD436221A0349006EAB84 /* Localizable.strings */; };
F8BDD43F221B5BAF006EAB84 /* TextsErrorMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD43E221B5BAF006EAB84 /* TextsErrorMessages.swift */; };
@ -435,6 +435,11 @@
F8EEDD5422FF685400D2D610 /* NSMutableURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EEDD5322FF685400D2D610 /* NSMutableURLRequest.swift */; };
F8EEDD552300136F00D2D610 /* DexcomShareTestResult.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8EEDD572300136F00D2D610 /* DexcomShareTestResult.strings */; };
F8EEDD6423020FAD00D2D610 /* NoCalibrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EEDD6323020FAD00D2D610 /* NoCalibrator.swift */; };
F8F7B8E6259A6EBF00C47B04 /* LibreSmoothing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F7B8E5259A6EBF00C47B04 /* LibreSmoothing.swift */; };
F8F7B8EB259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F7B8EA259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift */; };
F8F7B8F0259A850100C47B04 /* Double+KalmanFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F7B8EF259A850000C47B04 /* Double+KalmanFilter.swift */; };
F8F7B8F6259A852D00C47B04 /* KalmanFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F7B8F5259A852D00C47B04 /* KalmanFilter.swift */; };
F8F7B8FA259A857400C47B04 /* KalmanFilterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F7B8F9259A857400C47B04 /* KalmanFilterType.swift */; };
F8F971B623A5914D00C3F17D /* M5Stack+BluetoothPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971B123A5914C00C3F17D /* M5Stack+BluetoothPeripheral.swift */; };
F8F971B723A5914D00C3F17D /* BluetoothPeripheralType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971B423A5914C00C3F17D /* BluetoothPeripheralType.swift */; };
F8F971B823A5914D00C3F17D /* BluetoothPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971B523A5914C00C3F17D /* BluetoothPeripheral.swift */; };
@ -764,7 +769,7 @@
F85DC2F021CFE3D400B9F74A /* Calibration+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Calibration+CoreDataClass.swift"; sourceTree = "<group>"; };
F85DC2F121CFE3D400B9F74A /* Sensor+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sensor+CoreDataClass.swift"; sourceTree = "<group>"; };
F85DC2F221CFE3D400B9F74A /* BgReading+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BgReading+CoreDataClass.swift"; sourceTree = "<group>"; };
F85FB768255DE14600D1C39E /* ConstantsSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsSmoothing.swift; sourceTree = "<group>"; };
F85FB768255DE14600D1C39E /* ConstantsLibreSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsLibreSmoothing.swift; sourceTree = "<group>"; };
F85FF39025288870004E6FF1 /* HouseKeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HouseKeeper.swift; sourceTree = "<group>"; };
F85FF3C3252D0C32004E6FF1 /* xdrip v12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v12.xcdatamodel"; sourceTree = "<group>"; };
F85FF3CB252F9C9A004E6FF1 /* xdrip v13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v13.xcdatamodel"; sourceTree = "<group>"; };
@ -987,10 +992,10 @@
F8B48B1022B37C84009BCC01 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = "<group>"; };
F8B48B1122B37C84009BCC01 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/NightScoutTestResult.strings; sourceTree = "<group>"; };
F8B48B1222B37C84009BCC01 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/SettingsViews.strings; sourceTree = "<group>"; };
F8B955EA2591355200C06016 /* CGMLibre2Transmitter+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGMLibre2Transmitter+TestData.swift"; sourceTree = "<group>"; };
F8B95609259294FA00C06016 /* GlucoseDataFilterFlatValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDataFilterFlatValues.swift; sourceTree = "<group>"; };
F8B955B0258BEE9D00C06016 /* ConstantsSpeakReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsSpeakReading.swift; sourceTree = "<group>"; };
F8B955B6258D5E2000C06016 /* ConstantsHealthKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsHealthKit.swift; sourceTree = "<group>"; };
F8B955EA2591355200C06016 /* CGMLibre2Transmitter+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGMLibre2Transmitter+TestData.swift"; sourceTree = "<group>"; };
F8B95609259294FA00C06016 /* GlucoseDataFilterFlatValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDataFilterFlatValues.swift; sourceTree = "<group>"; };
F8BDD4232218790E006EAB84 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
F8BDD435221A0005006EAB84 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = "<group>"; };
F8BDD437221A0349006EAB84 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1066,6 +1071,11 @@
F8EEDD612300139800D2D610 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/DexcomShareTestResult.strings; sourceTree = "<group>"; };
F8EEDD622300139A00D2D610 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/DexcomShareTestResult.strings; sourceTree = "<group>"; };
F8EEDD6323020FAD00D2D610 /* NoCalibrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCalibrator.swift; sourceTree = "<group>"; };
F8F7B8E5259A6EBF00C47B04 /* LibreSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreSmoothing.swift; sourceTree = "<group>"; };
F8F7B8EA259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavitzkyGolaySmoothableArray.swift; sourceTree = "<group>"; };
F8F7B8EF259A850000C47B04 /* Double+KalmanFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+KalmanFilter.swift"; sourceTree = "<group>"; };
F8F7B8F5259A852D00C47B04 /* KalmanFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KalmanFilter.swift; sourceTree = "<group>"; };
F8F7B8F9259A857400C47B04 /* KalmanFilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KalmanFilterType.swift; sourceTree = "<group>"; };
F8F971B123A5914C00C3F17D /* M5Stack+BluetoothPeripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "M5Stack+BluetoothPeripheral.swift"; sourceTree = "<group>"; };
F8F971B423A5914C00C3F17D /* BluetoothPeripheralType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothPeripheralType.swift; sourceTree = "<group>"; };
F8F971B523A5914C00C3F17D /* BluetoothPeripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothPeripheral.swift; sourceTree = "<group>"; };
@ -2145,7 +2155,7 @@
F8A389EC23342EB10010F405 /* ConstantsNightScout.swift */,
F8A1586022EDB844007F5B5D /* ConstantsNotifications.swift */,
F8E51D5E2448E2E8001C9E5A /* ConstantsShareWithLoop.swift */,
F85FB768255DE14600D1C39E /* ConstantsSmoothing.swift */,
F85FB768255DE14600D1C39E /* ConstantsLibreSmoothing.swift */,
F8A1586222EDB86E007F5B5D /* ConstantsSounds.swift */,
F8B955B0258BEE9D00C06016 /* ConstantsSpeakReading.swift */,
F8A1587022EDC865007F5B5D /* ConstantsSpeakReadingLanguages.swift */,
@ -2160,14 +2170,16 @@
F8EA6C8021B723A80082976B /* Utilities */ = {
isa = PBXGroup;
children = (
F85FF38F25288860004E6FF1 /* HouseKeeping */,
F8AF11F624B1E6D700AE5BA2 /* Errors */,
F8C5EBE622F38F0E00563B5F /* Trace.swift */,
F821CF69229FC22D005C1E43 /* Network */,
F8B3A7DD226E48C1004BA588 /* SoundPlayer */,
F8EA6CA821BBE3010082976B /* UniqueId.swift */,
F85FF38F25288860004E6FF1 /* HouseKeeping */,
F8F7B8EE259A84A300C47B04 /* KalmanFilter */,
F81F9FF722861E6D0028C70F /* KeyValueObserverTimeKeeper.swift */,
F821CF69229FC22D005C1E43 /* Network */,
F821CF8022A5C814005C1E43 /* RepeatingTimer.swift */,
F8F7B8E9259A7A9400C47B04 /* SavitzkyGolayFilter */,
F8B3A7DD226E48C1004BA588 /* SoundPlayer */,
F8C5EBE622F38F0E00563B5F /* Trace.swift */,
F8EA6CA821BBE3010082976B /* UniqueId.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2229,6 +2241,24 @@
path = Calibration;
sourceTree = "<group>";
};
F8F7B8E9259A7A9400C47B04 /* SavitzkyGolayFilter */ = {
isa = PBXGroup;
children = (
F8F7B8EA259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift */,
);
path = SavitzkyGolayFilter;
sourceTree = "<group>";
};
F8F7B8EE259A84A300C47B04 /* KalmanFilter */ = {
isa = PBXGroup;
children = (
F8F7B8EF259A850000C47B04 /* Double+KalmanFilter.swift */,
F8F7B8F5259A852D00C47B04 /* KalmanFilter.swift */,
F8F7B8F9259A857400C47B04 /* KalmanFilterType.swift */,
);
path = KalmanFilter;
sourceTree = "<group>";
};
F8F971AD23A5914C00C3F17D /* BluetoothPeripheral */ = {
isa = PBXGroup;
children = (
@ -2459,6 +2489,7 @@
F8A5EEAD25791F370085E660 /* Libre2BLEUtilities.swift */,
F8A5EEB1257CEC290085E660 /* LibreNFC.swift */,
F8A5EEC1257D18DC0085E660 /* LibreNFCDelegate.swift */,
F8F7B8E5259A6EBF00C47B04 /* LibreSmoothing.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3016,7 +3047,9 @@
F8C97850242A9FD500A09483 /* MiaoMiaoBluetoothPeripheralViewModel.swift in Sources */,
F816E10E2437EAC9009EE65B /* BlueReader+CoreDataProperties.swift in Sources */,
F8B3A830227F085A004BA588 /* SettingsTableViewCell.swift in Sources */,
F8F7B8F6259A852D00C47B04 /* KalmanFilter.swift in Sources */,
F82436FC24BE014000BED341 /* TextsLibreStates.swift in Sources */,
F8F7B8EB259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift in Sources */,
F830991C23C2909E005741DF /* Watlaa+CoreDataClass.swift in Sources */,
F808D2CC240328FA0084B5DB /* Bubble+CoreDataClass.swift in Sources */,
F8A1586122EDB844007F5B5D /* ConstantsNotifications.swift in Sources */,
@ -3037,6 +3070,7 @@
F897AAFB2201018800CDDD10 /* String.swift in Sources */,
F8B3A847227F090E004BA588 /* SettingsViewNightScoutSettingsViewModel.swift in Sources */,
F8B3A79622635A25004BA588 /* AlertEntry+CoreDataClass.swift in Sources */,
F8F7B8E6259A6EBF00C47B04 /* LibreSmoothing.swift in Sources */,
F808D2CA240325E40084B5DB /* CGMBubbleTransmitterDelegate.swift in Sources */,
F804870C2336D90200EBDDB7 /* M5Stack+CoreDataClass.swift in Sources */,
F825286C2443BEDC0067AF77 /* CGMG6TransmitterDelegate.swift in Sources */,
@ -3053,7 +3087,8 @@
F8691888239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift in Sources */,
F8297F4F238DCAD800D74D66 /* BluetoothPeripheralNavigationController.swift in Sources */,
F8A1585322EDB602007F5B5D /* ConstantsBloodGlucose.swift in Sources */,
F85FB769255DE14600D1C39E /* ConstantsSmoothing.swift in Sources */,
F85FB769255DE14600D1C39E /* ConstantsLibreSmoothing.swift in Sources */,
F8F7B8FA259A857400C47B04 /* KalmanFilterType.swift in Sources */,
F8DF766023E38FC100063910 /* BLEPeripheral+CoreDataClass.swift in Sources */,
F80ED2EE236F68F90005C035 /* SettingsViewM5StackWiFiSettingsViewModel.swift in Sources */,
F8FDD6CB2553385000625B49 /* Array.swift in Sources */,
@ -3180,6 +3215,7 @@
F80859272364355F00F3829D /* ConstantsGlucoseChart.swift in Sources */,
F825286E2443C1000067AF77 /* BluetoothPeripheralManager+CGMG6TransmitterDelegate.swift in Sources */,
F816E10324367389009EE65B /* GNSEntryBluetoothPeripheralViewModel.swift in Sources */,
F8F7B8F0259A850100C47B04 /* Double+KalmanFilter.swift in Sources */,
F8E51D5D2448D8B5001C9E5A /* LoopManager.swift in Sources */,
F8DF766D23ED9B0900063910 /* DexcomG5BluetoothPeripheralViewModel.swift in Sources */,
F8E51D65244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift in Sources */,

View File

@ -1,6 +1,6 @@
import Foundation
extension GlucoseData: Smoothable {
extension GlucoseData: SavitzkyGolaySmoothable {
var value: Double {
get {

View File

@ -148,9 +148,9 @@ class Libre2BLEUtilities {
appendPreviousValues(to: &rawGlucoseValues, rawTemperatureValues: &rawTemperatureValues, temperatureAdjustmentValues: &temperatureAdjustmentValues)
// store current values (appended with previous values) in userdefaults prevous values
UserDefaults.standard.previousRawGlucoseValues = Array(rawGlucoseValues[0..<(min(rawGlucoseValues.count, ConstantsSmoothing.amountOfPreviousReadingsToStore))])
UserDefaults.standard.previousTemperatureAdjustmentValues = Array(temperatureAdjustmentValues[0..<(min(rawGlucoseValues.count, ConstantsSmoothing.amountOfPreviousReadingsToStore))])
UserDefaults.standard.previousRawTemperatureValues = Array(rawTemperatureValues[0..<(min(rawGlucoseValues.count, ConstantsSmoothing.amountOfPreviousReadingsToStore))])
UserDefaults.standard.previousRawGlucoseValues = Array(rawGlucoseValues[0..<(min(rawGlucoseValues.count, ConstantsLibreSmoothing.amountOfPreviousReadingsToStore))])
UserDefaults.standard.previousTemperatureAdjustmentValues = Array(temperatureAdjustmentValues[0..<(min(rawGlucoseValues.count, ConstantsLibreSmoothing.amountOfPreviousReadingsToStore))])
UserDefaults.standard.previousRawTemperatureValues = Array(rawTemperatureValues[0..<(min(rawGlucoseValues.count, ConstantsLibreSmoothing.amountOfPreviousReadingsToStore))])
// create glucosedata for each known rawglucose and add to returnvallue
for (index, _) in rawGlucoseValues.enumerated() {

View File

@ -101,26 +101,19 @@ class LibreDataParser {
return (max(0, (Double)(sensorTimeInMinutes - index))) * 60.0
}, 28)
// add previously stored values if there are any
trend = extendWithPreviousRawValues(trend: trend)
// now, if previousRawValues was not an empty list, trend is a longer list of values because it's been extended with a subrange of previousRawvalues
// we re-assign previousRawValues to the current list in trend, for next usage
// but we restricted it to maximum x most recent values, it makes no sense to store more
previousRawValues = Array(trend.map({$0.glucoseLevelRaw})[0..<(min(trend.count, ConstantsLibreSmoothing.amountOfPreviousReadingsToStore))])
// smooth, if required
if UserDefaults.standard.smoothLibreValues {
// add previously stored values if there are any
trend = extendWithPreviousRawValues(trend: trend)
// now, if previousRawValues was not an empty list, trend is a longer list of values because it's been extended with a subrange of previousRawvalues
// we re-assign previousRawValues to the current list in trend, for next usage
// but we restricted it to maximum x most recent values, it makes no sense to store more
previousRawValues = Array(trend.map({$0.glucoseLevelRaw})[0..<(min(trend.count, ConstantsSmoothing.amountOfPreviousReadingsToStore))])
// smooth the trend values, filterWidth 5, 2 iterations
for _ in 1...ConstantsSmoothing.libreSmoothingRepeatPerMinuteSmoothing {
trend.smoothSavitzkyGolayQuaDratic(withFilterWidth: ConstantsSmoothing.libreSmoothingFilterWidthPerMinuteValues)
}
// now smooth the trend, per 5 minutes smoothing, 3 iterations, filterWidth 3
smoothPer5Minutes(trend: trend, withFilterWidth: ConstantsSmoothing.libreSmoothingFilterWidthPer5MinuteValues, iterations: ConstantsSmoothing.libreSmoothingRepeatPer5MinuteSmoothing)
// apply SavitzkyGolayFilter
LibreSmoothing.smooth(trend: &trend, repeatPerMinuteSmoothingSavitzkyGolay: ConstantsLibreSmoothing.libreSmoothingRepeatPerMinuteSmoothing, filterWidthPerMinuteValuesSavitzkyGolay: ConstantsLibreSmoothing.filterWidthPerMinuteValues, filterWidthPer5MinuteValuesSavitzkyGolay: ConstantsLibreSmoothing.filterWidthPer5MinuteValues, repeatPer5MinuteSmoothingSavitzkyGolay: ConstantsLibreSmoothing.repeatPer5MinuteSmoothing)
}
@ -149,7 +142,7 @@ class LibreDataParser {
history.insert(GlucoseData(timeStamp: trend[0].timeStamp, glucoseLevelRaw: trend[0].glucoseLevelRaw), at: 0)
// smooth
history.smoothSavitzkyGolayQuaDratic(withFilterWidth: ConstantsSmoothing.libreSmoothingFilterWidthPer15MinutesValues)
LibreSmoothing.smooth(history: &history, filterWidthPer5MinuteSmoothingSavitzkyGolay: ConstantsLibreSmoothing.libreSmoothingFilterWidthPer15MinutesValues)
// now remove the trend measurement that was inserted
history.remove(at: 0)
@ -493,7 +486,7 @@ class LibreDataParser {
// previousRawValues length must be at least 16 and trend length must equal to 16 values, should always be the case, just to avoid crashes
guard previousRawValues.count >= 16 && trend.count == 16 else {return trend}
// create a new array with IsSmoothable objects, values being equal to glucoseLevelRaw of each trend value
// create a new array with new GlucoseData instances
var newTrend = trend.map({GlucoseData(timeStamp: $0.timeStamp, glucoseLevelRaw: $0.glucoseLevelRaw)})
// for each value in trend, we will try to find a series of 4 (defined by amountOfValuesToCompare) matching values in previousRawValues
@ -539,66 +532,6 @@ class LibreDataParser {
}
/// - smooths each value, using values of 5 minutes before , 10 minutes before, 5 minutes after and 10 minutes after, ... the number of values taken into account depends on the filterWidth
/// - parameters :
/// - withFilterWidth : filter width to use
/// - repeat : how often to redo the filter
/// - trend : glucoseData array to filter, objects in the array will be smoothed (= filtered)
private func smoothPer5Minutes(trend: [GlucoseData], withFilterWidth filterWidth: Int, iterations: Int) {
// trend must both have at least 16 values, should always be the case, just to avoid crashes
guard trend.count >= 16 else {return}
// copy glucose values to array of double
let smoothedValues = trend.map({$0.glucoseLevelRaw})
// now we have smoothedValues, Double's with values equal to trend's glucoseLevelRaw values
// we will apply smoothing, each value will be smoothed using the value if 5 minutes before, 10 minutes before, 15 minutes before, 5 minutes after and 10 minutes after and 15 minutes after - because we'll never use two subsequent values, we use them with an interval of 5 minutes and with a filterWidth of 2 .. and more
for (index, value) in trend.enumerated() {
// initalize toSmooth with value that will be smoothed
var toSmooth = [smoothedValues[index]]
// while adding values to toSmooth, we need to keep track of the index of the value being smoothed
var indexOfValueBeingSmoothed = 0
// prepend values 5 and 10 and 15 minutes ago, ... maximum 5 which is the maximum filterwidth
for count in 1...5 {
let indexToUse = index - 5 * count
if indexToUse >= 0 {
toSmooth.insert(smoothedValues[indexToUse], at: 0)
indexOfValueBeingSmoothed = count
}
}
// append values 5 and 10 and 15 minutes later, ... maximum 5 which is the maximum filterwidth
for count in 1...5 {
let indexToUse = index + 5 * count
if indexToUse < smoothedValues.count - 1 {
toSmooth.append(smoothedValues[indexToUse])
}
}
// smooth
for _ in 1...iterations {
toSmooth.smoothSavitzkyGolayQuaDratic(withFilterWidth: filterWidth)
}
// now change the value being smoothed
value.glucoseLevelRaw = toSmooth[indexOfValueBeingSmoothed]
}
}
}

View File

@ -0,0 +1,146 @@
import Foundation
class LibreSmoothing {
// MARK: - public functions
/// - smooths trend array of GlucoseData, ie per minute values using SavitzkyGolayQuaDratic filter
/// - first per minute SavitzkyGolayQuaDratic, then Kalman filter
/// - after Kalman filter, back the per 5 minutes : using values of 5 minutes before , 10 minutes before, 5 minutes after and 10 minutes after, ... the number of values taken into account depends on the filterWidth
public static func smooth(trend: inout [GlucoseData], repeatPerMinuteSmoothingSavitzkyGolay: Int, filterWidthPerMinuteValuesSavitzkyGolay: Int, filterWidthPer5MinuteValuesSavitzkyGolay: Int, repeatPer5MinuteSmoothingSavitzkyGolay:Int) {
debuglogging("trend before savitzkygolay filter")
for (_, value) in trend.enumerated().reversed() {
debuglogging("value = " + value.glucoseLevelRaw.description.replacingOccurrences(of: ".", with: ","))
}
// smooth the trend values, filterWidth 5, 2 iterations
for _ in 1...repeatPerMinuteSmoothingSavitzkyGolay {
trend.smoothSavitzkyGolayQuaDratic(withFilterWidth: filterWidthPerMinuteValuesSavitzkyGolay)
}
debuglogging("trend before kalman filter")
for (_, value) in trend.enumerated().reversed() {
debuglogging("value = " + value.glucoseLevelRaw.description.replacingOccurrences(of: ".", with: ","))
}
// apply Kalman filter
LibreSmoothing.smoothWithKalmanFilter(trend: &trend, filterNoise: 2.5)
debuglogging("trend after kalman filter")
for (_, value) in trend.enumerated().reversed() {
debuglogging("value = " + value.glucoseLevelRaw.description.replacingOccurrences(of: ".", with: ","))
}
// now smooth per 5 minutes
LibreSmoothing.smoothPer5Minutes(trend: trend, withFilterWidth: filterWidthPer5MinuteValuesSavitzkyGolay, iterations: repeatPer5MinuteSmoothingSavitzkyGolay)
}
/// - smooths history array of GlucoseData, ie per 15 minute values using SavitzkyGolayQuaDratic filter
public static func smooth(history: inout [GlucoseData], filterWidthPer5MinuteSmoothingSavitzkyGolay: Int) {
history.smoothSavitzkyGolayQuaDratic(withFilterWidth: filterWidthPer5MinuteSmoothingSavitzkyGolay)
}
// MARK: - private functions
/// trend must be list of non 0 values, equal time distance from each other (ie every minute)
private static func smoothWithKalmanFilter(trend: inout [GlucoseData], filterNoise: Double) {
// there must be at least one element
guard trend.count > 0 else {return}
// all values must be > 0.0
guard trend.filter({return $0.glucoseLevelRaw > 0.0}).count > 0 else {return}
// copy glucoseLevelRaw for each element in trend to array of Double
let trendAsDoubleArray = trend.map({$0.glucoseLevelRaw})
var filter = KalmanFilter(stateEstimatePrior: trendAsDoubleArray[0], errorCovariancePrior: filterNoise)
// iterate through the items reversed, because the first item is actually the most recent
for (index, item) in trendAsDoubleArray.enumerated().reversed() {
let prediction = filter.predict(stateTransitionModel: 1, controlInputModel: 0, controlVector: 0, covarianceOfProcessNoise: filterNoise)
let update = prediction.update(measurement: item, observationModel: 1, covarienceOfObservationNoise: filterNoise)
filter = update
let glucose = filter.stateEstimatePrior
guard (glucose > 0.0) else {
break
}
trend[index].glucoseLevelRaw = glucose
}
}
/// - smooths each value, using values of 5 minutes before , 10 minutes before, 5 minutes after and 10 minutes after, ... the number of values taken into account depends on the filterWidth
/// - parameters :
/// - withFilterWidth : filter width to use
/// - repeat : how often to redo the filter
/// - trend : glucoseData array to filter, objects in the array will be smoothed (= filtered)
private static func smoothPer5Minutes(trend: [GlucoseData], withFilterWidth filterWidth: Int, iterations: Int) {
// trend must both have at least 16 values, should always be the case, just to avoid crashes
guard trend.count >= 16 else {return}
// copy glucose values to array of double
let smoothedValues = trend.map({$0.glucoseLevelRaw})
// now we have smoothedValues, Double's with values equal to trend's glucoseLevelRaw values
// we will apply smoothing, each value will be smoothed using the value if 5 minutes before, 10 minutes before, 15 minutes before, 5 minutes after and 10 minutes after and 15 minutes after - because we'll never use two subsequent values, we use them with an interval of 5 minutes and with a filterWidth of 2 .. and more
for (index, value) in trend.enumerated() {
// initalize toSmooth with value that will be smoothed
var toSmooth = [smoothedValues[index]]
// while adding values to toSmooth, we need to keep track of the index of the value being smoothed
var indexOfValueBeingSmoothed = 0
// prepend values 5 and 10 and 15 minutes ago, ... maximum 5 which is the maximum filterwidth
for count in 1...5 {
let indexToUse = index - 5 * count
if indexToUse >= 0 {
toSmooth.insert(smoothedValues[indexToUse], at: 0)
indexOfValueBeingSmoothed = count
}
}
// append values 5 and 10 and 15 minutes later, ... maximum 5 which is the maximum filterwidth
for count in 1...5 {
let indexToUse = index + 5 * count
if indexToUse < smoothedValues.count - 1 {
toSmooth.append(smoothedValues[indexToUse])
}
}
// smooth
for _ in 1...iterations {
toSmooth.smoothSavitzkyGolayQuaDratic(withFilterWidth: filterWidth)
}
// now change the value being smoothed
value.glucoseLevelRaw = toSmooth[indexOfValueBeingSmoothed]
}
}
}

View File

@ -1,11 +1,11 @@
import Foundation
enum ConstantsSmoothing {
enum ConstantsLibreSmoothing {
/// - The first 16 readings of Libre (the trend) will be smoothed using Savitzky Golay Quadratic filter (if smoothing enabled)
/// - this smoothing happens in the LibreDataParser, before they are being sent to the delegate (ie the RootViewController)
/// - this value defines the filter width to use
static let libreSmoothingFilterWidthPerMinuteValues = 5
static let filterWidthPerMinuteValues = 5
/// how many times to do the smoothing off the per minute values
static let libreSmoothingRepeatPerMinuteSmoothing = 2
@ -13,10 +13,10 @@ enum ConstantsSmoothing {
/// - The first 16 readings of Libre will be extended with each receipt of new readings, extended with stored values of prevous reading
/// - an additional smoothing will be done on per 5 minute values
/// - this value defines the filter width to use
static let libreSmoothingFilterWidthPer5MinuteValues = 3
static let filterWidthPer5MinuteValues = 3
/// how many times to do the smoothing off the per 5 minutes values
static let libreSmoothingRepeatPer5MinuteSmoothing = 3
static let repeatPer5MinuteSmoothing = 3
/// - The last 32 readings of Libre (the history) will be smoothed using Savitzky Golay Quadratic filter (if smoothing enabled)
/// - this smoothing happens in the LibreDataParser, before they are being sent to the delegate (ie the RootViewController)

View File

@ -1,162 +1,6 @@
import Foundation
import CoreML
/// allowed values are 0, 1, 2 or 3. It's the index in coefficients
fileprivate var coefficientsRowToUse = 3
/// Savitzky Golay coefficients
fileprivate let coefficients = [[ -3.0, 12.0, 17.0, 12.0, -3.0],
[ -2.0, 3.0, 6.0, 7.0, 6.0, 3.0, -2.0],
[ -21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0],
[ -36.0, 9.0, 44.0, 69.0, 84.0, 89.0, 84.0, 69.0, 44.0, 9.0, -36.0]]
/// an array with elments of a type that conforms to Smoothable, can be filtered using the Savitzky Golay algorithm
protocol Smoothable {
/// value to be smoothed
var value: Double { get set }
}
/// local help class
class IsSmoothable: Smoothable {
var value: Double = 0.0
init(withValue value: Double = 0.0) {
self.value = value
}
}
extension Array where Element: Smoothable {
/// - apply Savitzky Golay filter
/// - before applying the filter, the array will be prepended and append with a number of elements equal to the filterwidth, filterWidth default 5. Allowed values are 5, 4, 3, 2. If any other value is assigned, then 5 will be used
/// - ...continue with 5 here in the explanation ...
/// - for the 5 last elements and 5 first elements, a regression is done. This regression is done used to give values to the 5 prepended and appended values. Which means it's as if we draw a line through the first 5 and 5 last original values, and use this line to give values to the 5 prepended and appended values
/// - the 5 prepended and appended values are then used in the filter algorithm, which means we can also filter the original 5 first and last elements
/// see also example https://github.com/JohanDegraeve/xdripswift/wiki/Libre-value-smoothing
mutating func smoothSavitzkyGolayQuaDratic(withFilterWidth filterWidth: Int = 5) {
// filterWidthToUse is the value of filterWidth to use in the algorithm. By default filterWidthToUse = parameter value filterWidth
var filterWidthToUse = filterWidth
// calculate coefficientsRowToUse based on filterWdith
switch filterWidth {
case 5:
coefficientsRowToUse = 3
case 4:
coefficientsRowToUse = 2
case 3:
coefficientsRowToUse = 1
case 2:
coefficientsRowToUse = 0
default:
// invalid filterWidth was given in parameterList, use default value
coefficientsRowToUse = 3
filterWidthToUse = 5
}
// using 5 here in the comments as value for filterWidthToUse
// the amount of elements must be at least 5. If that's not the case then don't apply any smoothing
guard self.count >= filterWidthToUse else {return}
// create a new array, to which we will prepend and append 5 elements so that we can do also smoothing for the 5 last and 5 first values of the input array (which is self)
// the 5 elements will be estimated by doing linear regression of the first 5 and last 5 elements of the original input array respectively
// this is only a temporary array, but it will hold the elements of the original array, those elements will get a new value when doing the smoothing
var tempArray = [Smoothable]()
for element in self {
tempArray.append(element)
}
// now prepend and append with 5 elements, each with a default value 0.0
for _ in 0..<filterWidthToUse {
tempArray.insert(IsSmoothable(), at: 0)
tempArray.append(IsSmoothable())
}
// so now we have tempArray, of length size of original array + 2 * 5
// the first 5 and the last 5 elements are of type IsSmoothable with value 0
// - indicesArray is a help array needed for the function linearRegressionCreator
// - this will be the first parameter in the call to the linearRegression function, in fact it's an array of IsSmoothable with length = length of tempArray
// - we give each IsSmoothable the value of the index, meaning from 0 up to (length of tempArray) - 1
// - in fact it's not really smoothable, it's just because we use isSmoothable in function linearRegressionCreator
var indicesArray = [Smoothable]()
for index in 0..<(self.count + (filterWidthToUse * 2)) {
indicesArray.append(IsSmoothable(withValue: Double(index)))
}
/// - this is a piece of code that we will execute two times, once for the firs 5 elements, then for the last 5, so we put it in a closure variable
/// - it calculates the regression function (which is nothing else but doing y = intercept + slope*x) for range defined by predictorRange in tempArray. It will be used for the 5 first and 5 last real values, ie the 5 first and 5 last real glucose values
/// - then executes the regression for every element in the range defined by targetRange, again in tempArray
let doRegression = { (predictorRange: Range<Int>, targetRange: Range<Int>) in
// calculate the linearRegression function
let linearRegression = linearRegressionCreator(indicesArray[predictorRange], tempArray[predictorRange])
// ready to do the linear regression for the targetRange in tempArray
for index in targetRange {
tempArray[index].value = linearRegression(indicesArray[index].value)
}
}
// now do the regression for the 5 first elements
doRegression(filterWidthToUse..<(filterWidthToUse * 2), 0..<filterWidthToUse)
// now do the regression for the 5 last elements
doRegression((tempArray.count - filterWidthToUse * 2)..<(tempArray.count - filterWidthToUse), (tempArray.count - filterWidthToUse)..<tempArray.count)
// now start filtering
// initialize array that will hold the resulting filtered values
var filteredValues = [Double]()
// calculate divider
let divider = coefficients[coefficientsRowToUse].reduce(0, { x, y in
x + y
})
// filter each original value
for _ in 0..<self.count {
// add a new element to filteredValues, start value is 0.0
// this new value will be the last element, so we access it with index filteredValues.count - 1
filteredValues.append(0.0)
// iterate through the coefficients
for (index, coefficient) in coefficients[coefficientsRowToUse].enumerated() {
filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] + coefficient * tempArray[index + filteredValues.count - 1].value
}
filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] / divider
}
// now assign the new values to the original objects
for (index, _) in self.enumerated() {
self[index].value = filteredValues[index]
}
}
}
extension Array where Element: GlucoseData {
/// - GlucoseData array has values with glucoseLevelRaw = 0.0 - this function will do extrapolation of prevous and next non 0 values to estimate/fill up 0 values
@ -321,26 +165,3 @@ extension Array where Element: BgReading {
}
/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
fileprivate func multiply(_ a: ArraySlice<Smoothable>, _ b: ArraySlice<Smoothable>) -> ArraySlice<Smoothable> {
return zip(a,b).map({IsSmoothable(withValue: $0.value * $1.value)})[0..<a.count]
}
/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
fileprivate func average(_ input: ArraySlice<Smoothable>) -> Double {
return (input.reduce(IsSmoothable(), { (x: Smoothable, y:Smoothable) in
IsSmoothable(withValue: x.value + y.value)})).value / Double(input.count)
}
/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
fileprivate func linearRegressionCreator(_ xs: ArraySlice<Smoothable>, _ ys: ArraySlice<Smoothable>) -> (Double) -> Double {
let sum1 = average(multiply(ys, xs)) - average(xs) * average(ys)
let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
let slope = sum1 / sum2
let intercept = average(ys) - slope * average(xs)
return { x in intercept + slope * x }
}

View File

@ -1,6 +1,6 @@
import Foundation
extension Double: Smoothable {
extension Double: SavitzkyGolaySmoothable {
var value: Double {
get {

View File

@ -0,0 +1,6 @@
// MARK: Double as Kalman input
extension Double: KalmanInput {
public var transposed: Double { self }
public var inversed: Double { 1 / self }
public var additionToUnit: Double { 1 - self }
}

View File

@ -0,0 +1,61 @@
/**
Conventional Kalman Filter
*/
public struct KalmanFilter<Type: KalmanInput>: KalmanFilterType {
/// x̂_k|k-1
public let stateEstimatePrior: Type
/// P_k|k-1
public let errorCovariancePrior: Type
public init(stateEstimatePrior: Type, errorCovariancePrior: Type) {
self.stateEstimatePrior = stateEstimatePrior
self.errorCovariancePrior = errorCovariancePrior
}
/**
Predict step in Kalman filter.
- parameter stateTransitionModel: F_k
- parameter controlInputModel: B_k
- parameter controlVector: u_k
- parameter covarianceOfProcessNoise: Q_k
- returns: Another instance of Kalman filter with predicted x̂_k and P_k
*/
public func predict(stateTransitionModel: Type, controlInputModel: Type, controlVector: Type, covarianceOfProcessNoise: Type) -> KalmanFilter {
// x̂_k|k-1 = F_k * x̂_k-1|k-1 + B_k * u_k
let predictedStateEstimate = stateTransitionModel * stateEstimatePrior + controlInputModel * controlVector
// P_k|k-1 = F_k * P_k-1|k-1 * F_k^t + Q_k
let predictedEstimateCovariance = stateTransitionModel * errorCovariancePrior * stateTransitionModel.transposed + covarianceOfProcessNoise
return KalmanFilter(stateEstimatePrior: predictedStateEstimate, errorCovariancePrior: predictedEstimateCovariance)
}
/**
Update step in Kalman filter. We update our prediction with the measurements that we make
- parameter measurement: z_k
- parameter observationModel: H_k
- parameter covarienceOfObservationNoise: R_k
- returns: Updated with the measurements version of Kalman filter with new x̂_k and P_k
*/
public func update(measurement: Type, observationModel: Type, covarienceOfObservationNoise: Type) -> KalmanFilter {
// H_k^t transposed. We cache it improve performance
let observationModelTransposed = observationModel.transposed
// _k = z_k - H_k * x̂_k|k-1
let measurementResidual = measurement - observationModel * stateEstimatePrior
// S_k = H_k * P_k|k-1 * H_k^t + R_k
let residualCovariance = observationModel * errorCovariancePrior * observationModelTransposed + covarienceOfObservationNoise
// K_k = P_k|k-1 * H_k^t * S_k^-1
let kalmanGain = errorCovariancePrior * observationModelTransposed * residualCovariance.inversed
// x̂_k|k = x̂_k|k-1 + K_k * _k
let posterioriStateEstimate = stateEstimatePrior + kalmanGain * measurementResidual
// P_k|k = (I - K_k * H_k) * P_k|k-1
let posterioriEstimateCovariance = (kalmanGain * observationModel).additionToUnit * errorCovariancePrior
return KalmanFilter(stateEstimatePrior: posterioriStateEstimate, errorCovariancePrior: posterioriEstimateCovariance)
}
}

View File

@ -0,0 +1,19 @@
public protocol KalmanInput {
var transposed: Self { get }
var inversed: Self { get }
var additionToUnit: Self { get }
static func + (lhs: Self, rhs: Self) -> Self
static func - (lhs: Self, rhs: Self) -> Self
static func * (lhs: Self, rhs: Self) -> Self
}
public protocol KalmanFilterType {
associatedtype Input: KalmanInput
var stateEstimatePrior: Input { get }
var errorCovariancePrior: Input { get }
func predict(stateTransitionModel: Input, controlInputModel: Input, controlVector: Input, covarianceOfProcessNoise: Input) -> Self
func update(measurement: Input, observationModel: Input, covarienceOfObservationNoise: Input) -> Self
}

View File

@ -0,0 +1,181 @@
import Foundation
/// allowed values are 0, 1, 2 or 3. It's the index in coefficients
fileprivate var coefficientsRowToUse = 3
/// Savitzky Golay coefficients
fileprivate let coefficients = [[ -3.0, 12.0, 17.0, 12.0, -3.0],
[ -2.0, 3.0, 6.0, 7.0, 6.0, 3.0, -2.0],
[ -21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0],
[ -36.0, 9.0, 44.0, 69.0, 84.0, 89.0, 84.0, 69.0, 44.0, 9.0, -36.0]]
/// an array with elements of a type that conforms to Smoothable, can be filtered using the Savitzky Golay algorithm
protocol SavitzkyGolaySmoothable {
/// value to be smoothed
var value: Double { get set }
}
/// local help class
fileprivate class IsSmoothable: SavitzkyGolaySmoothable {
var value: Double = 0.0
init(withValue value: Double = 0.0) {
self.value = value
}
}
extension Array where Element: SavitzkyGolaySmoothable {
/// - apply Savitzky Golay filter
/// - before applying the filter, the array will be prepended and append with a number of elements equal to the filterwidth, filterWidth default 5. Allowed values are 5, 4, 3, 2. If any other value is assigned, then 5 will be used
/// - ...continue with 5 here in the explanation ...
/// - for the 5 last elements and 5 first elements, a regression is done. This regression is done used to give values to the 5 prepended and appended values. Which means it's as if we draw a line through the first 5 and 5 last original values, and use this line to give values to the 5 prepended and appended values
/// - the 5 prepended and appended values are then used in the filter algorithm, which means we can also filter the original 5 first and last elements
/// see also example https://github.com/JohanDegraeve/xdripswift/wiki/Libre-value-smoothing
mutating func smoothSavitzkyGolayQuaDratic(withFilterWidth filterWidth: Int = 5) {
// filterWidthToUse is the value of filterWidth to use in the algorithm. By default filterWidthToUse = parameter value filterWidth
var filterWidthToUse = filterWidth
// calculate coefficientsRowToUse based on filterWdith
switch filterWidth {
case 5:
coefficientsRowToUse = 3
case 4:
coefficientsRowToUse = 2
case 3:
coefficientsRowToUse = 1
case 2:
coefficientsRowToUse = 0
default:
// invalid filterWidth was given in parameterList, use default value
coefficientsRowToUse = 3
filterWidthToUse = 5
}
// using 5 here in the comments as value for filterWidthToUse
// the amount of elements must be at least 5. If that's not the case then don't apply any smoothing
guard self.count >= filterWidthToUse else {return}
// create a new array, to which we will prepend and append 5 elements so that we can do also smoothing for the 5 last and 5 first values of the input array (which is self)
// the 5 elements will be estimated by doing linear regression of the first 5 and last 5 elements of the original input array respectively
// this is only a temporary array, but it will hold the elements of the original array, those elements will get a new value when doing the smoothing
var tempArray = [SavitzkyGolaySmoothable]()
for element in self {
tempArray.append(element)
}
// now prepend and append with 5 elements, each with a default value 0.0
for _ in 0..<filterWidthToUse {
tempArray.insert(IsSmoothable(), at: 0)
tempArray.append(IsSmoothable())
}
// so now we have tempArray, of length size of original array + 2 * 5
// the first 5 and the last 5 elements are of type IsSmoothable with value 0
// - indicesArray is a help array needed for the function linearRegressionCreator
// - this will be the first parameter in the call to the linearRegression function, in fact it's an array of IsSmoothable with length = length of tempArray
// - we give each IsSmoothable the value of the index, meaning from 0 up to (length of tempArray) - 1
// - in fact it's not really smoothable, it's just because we use isSmoothable in function linearRegressionCreator
var indicesArray = [SavitzkyGolaySmoothable]()
for index in 0..<(self.count + (filterWidthToUse * 2)) {
indicesArray.append(IsSmoothable(withValue: Double(index)))
}
/// - this is a piece of code that we will execute two times, once for the firs 5 elements, then for the last 5, so we put it in a closure variable
/// - it calculates the regression function (which is nothing else but doing y = intercept + slope*x) for range defined by predictorRange in tempArray. It will be used for the 5 first and 5 last real values, ie the 5 first and 5 last real glucose values
/// - then executes the regression for every element in the range defined by targetRange, again in tempArray
let doRegression = { (predictorRange: Range<Int>, targetRange: Range<Int>) in
// calculate the linearRegression function
let linearRegression = linearRegressionCreator(indicesArray[predictorRange], tempArray[predictorRange])
// ready to do the linear regression for the targetRange in tempArray
for index in targetRange {
tempArray[index].value = linearRegression(indicesArray[index].value)
}
}
// now do the regression for the 5 first elements
doRegression(filterWidthToUse..<(filterWidthToUse * 2), 0..<filterWidthToUse)
// now do the regression for the 5 last elements
doRegression((tempArray.count - filterWidthToUse * 2)..<(tempArray.count - filterWidthToUse), (tempArray.count - filterWidthToUse)..<tempArray.count)
// now start filtering
// initialize array that will hold the resulting filtered values
var filteredValues = [Double]()
// calculate divider
let divider = coefficients[coefficientsRowToUse].reduce(0, { x, y in
x + y
})
// filter each original value
for _ in 0..<self.count {
// add a new element to filteredValues, start value is 0.0
// this new value will be the last element, so we access it with index filteredValues.count - 1
filteredValues.append(0.0)
// iterate through the coefficients
for (index, coefficient) in coefficients[coefficientsRowToUse].enumerated() {
filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] + coefficient * tempArray[index + filteredValues.count - 1].value
}
filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] / divider
}
// now assign the new values to the original objects
for (index, _) in self.enumerated() {
self[index].value = filteredValues[index]
}
}
}
/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
fileprivate func multiply(_ a: ArraySlice<SavitzkyGolaySmoothable>, _ b: ArraySlice<SavitzkyGolaySmoothable>) -> ArraySlice<SavitzkyGolaySmoothable> {
return zip(a,b).map({IsSmoothable(withValue: $0.value * $1.value)})[0..<a.count]
}
/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
fileprivate func average(_ input: ArraySlice<SavitzkyGolaySmoothable>) -> Double {
return (input.reduce(IsSmoothable(), { (x: SavitzkyGolaySmoothable, y:SavitzkyGolaySmoothable) in
IsSmoothable(withValue: x.value + y.value)})).value / Double(input.count)
}
/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
fileprivate func linearRegressionCreator(_ xs: ArraySlice<SavitzkyGolaySmoothable>, _ ys: ArraySlice<SavitzkyGolaySmoothable>) -> (Double) -> Double {
let sum1 = average(multiply(ys, xs)) - average(xs) * average(ys)
let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
let slope = sum1 / sum2
let intercept = average(ys) - slope * average(xs)
return { x in intercept + slope * x }
}

View File

@ -630,7 +630,7 @@ final class RootViewController: UIViewController {
// start defining timeStampToDelete as of when existing BgReading's will be deleted
// this value is also used to verify that glucoseData Array has enough readings
var timeStampToDelete = Date(timeIntervalSinceNow: -60.0 * (Double)(ConstantsSmoothing.readingsToDeleteInMinutes))
var timeStampToDelete = Date(timeIntervalSinceNow: -60.0 * (Double)(ConstantsLibreSmoothing.readingsToDeleteInMinutes))
// now check if we'll delete readings
// there must be a glucoseData.last, here assigning lastGlucoseData just to unwrap it