diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index 6055f8d4..45465479 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -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 = ""; }; F85DC2F121CFE3D400B9F74A /* Sensor+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sensor+CoreDataClass.swift"; sourceTree = ""; }; F85DC2F221CFE3D400B9F74A /* BgReading+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BgReading+CoreDataClass.swift"; sourceTree = ""; }; - F85FB768255DE14600D1C39E /* ConstantsSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsSmoothing.swift; sourceTree = ""; }; + F85FB768255DE14600D1C39E /* ConstantsLibreSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsLibreSmoothing.swift; sourceTree = ""; }; F85FF39025288870004E6FF1 /* HouseKeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HouseKeeper.swift; sourceTree = ""; }; F85FF3C3252D0C32004E6FF1 /* xdrip v12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v12.xcdatamodel"; sourceTree = ""; }; F85FF3CB252F9C9A004E6FF1 /* xdrip v13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v13.xcdatamodel"; sourceTree = ""; }; @@ -987,10 +992,10 @@ F8B48B1022B37C84009BCC01 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = ""; }; F8B48B1122B37C84009BCC01 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/NightScoutTestResult.strings; sourceTree = ""; }; F8B48B1222B37C84009BCC01 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/SettingsViews.strings; sourceTree = ""; }; - F8B955EA2591355200C06016 /* CGMLibre2Transmitter+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGMLibre2Transmitter+TestData.swift"; sourceTree = ""; }; - F8B95609259294FA00C06016 /* GlucoseDataFilterFlatValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDataFilterFlatValues.swift; sourceTree = ""; }; F8B955B0258BEE9D00C06016 /* ConstantsSpeakReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsSpeakReading.swift; sourceTree = ""; }; F8B955B6258D5E2000C06016 /* ConstantsHealthKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsHealthKit.swift; sourceTree = ""; }; + F8B955EA2591355200C06016 /* CGMLibre2Transmitter+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGMLibre2Transmitter+TestData.swift"; sourceTree = ""; }; + F8B95609259294FA00C06016 /* GlucoseDataFilterFlatValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDataFilterFlatValues.swift; sourceTree = ""; }; F8BDD4232218790E006EAB84 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; F8BDD435221A0005006EAB84 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; F8BDD437221A0349006EAB84 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -1066,6 +1071,11 @@ F8EEDD612300139800D2D610 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/DexcomShareTestResult.strings; sourceTree = ""; }; F8EEDD622300139A00D2D610 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/DexcomShareTestResult.strings; sourceTree = ""; }; F8EEDD6323020FAD00D2D610 /* NoCalibrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCalibrator.swift; sourceTree = ""; }; + F8F7B8E5259A6EBF00C47B04 /* LibreSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreSmoothing.swift; sourceTree = ""; }; + F8F7B8EA259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavitzkyGolaySmoothableArray.swift; sourceTree = ""; }; + F8F7B8EF259A850000C47B04 /* Double+KalmanFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+KalmanFilter.swift"; sourceTree = ""; }; + F8F7B8F5259A852D00C47B04 /* KalmanFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KalmanFilter.swift; sourceTree = ""; }; + F8F7B8F9259A857400C47B04 /* KalmanFilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KalmanFilterType.swift; sourceTree = ""; }; F8F971B123A5914C00C3F17D /* M5Stack+BluetoothPeripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "M5Stack+BluetoothPeripheral.swift"; sourceTree = ""; }; F8F971B423A5914C00C3F17D /* BluetoothPeripheralType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothPeripheralType.swift; sourceTree = ""; }; F8F971B523A5914C00C3F17D /* BluetoothPeripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothPeripheral.swift; sourceTree = ""; }; @@ -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 = ""; @@ -2229,6 +2241,24 @@ path = Calibration; sourceTree = ""; }; + F8F7B8E9259A7A9400C47B04 /* SavitzkyGolayFilter */ = { + isa = PBXGroup; + children = ( + F8F7B8EA259A7B1C00C47B04 /* SavitzkyGolaySmoothableArray.swift */, + ); + path = SavitzkyGolayFilter; + sourceTree = ""; + }; + F8F7B8EE259A84A300C47B04 /* KalmanFilter */ = { + isa = PBXGroup; + children = ( + F8F7B8EF259A850000C47B04 /* Double+KalmanFilter.swift */, + F8F7B8F5259A852D00C47B04 /* KalmanFilter.swift */, + F8F7B8F9259A857400C47B04 /* KalmanFilterType.swift */, + ); + path = KalmanFilter; + sourceTree = ""; + }; F8F971AD23A5914C00C3F17D /* BluetoothPeripheral */ = { isa = PBXGroup; children = ( @@ -2459,6 +2489,7 @@ F8A5EEAD25791F370085E660 /* Libre2BLEUtilities.swift */, F8A5EEB1257CEC290085E660 /* LibreNFC.swift */, F8A5EEC1257D18DC0085E660 /* LibreNFCDelegate.swift */, + F8F7B8E5259A6EBF00C47B04 /* LibreSmoothing.swift */, ); path = Utilities; sourceTree = ""; @@ -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 */, diff --git a/xdrip/BluetoothTransmitter/CGM/Generic/GlucoseData+Smoothable.swift b/xdrip/BluetoothTransmitter/CGM/Generic/GlucoseData+Smoothable.swift index 8de8383b..ca9ccdd1 100644 --- a/xdrip/BluetoothTransmitter/CGM/Generic/GlucoseData+Smoothable.swift +++ b/xdrip/BluetoothTransmitter/CGM/Generic/GlucoseData+Smoothable.swift @@ -1,6 +1,6 @@ import Foundation -extension GlucoseData: Smoothable { +extension GlucoseData: SavitzkyGolaySmoothable { var value: Double { get { diff --git a/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/Libre2BLEUtilities.swift b/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/Libre2BLEUtilities.swift index 5a674313..b657a040 100644 --- a/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/Libre2BLEUtilities.swift +++ b/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/Libre2BLEUtilities.swift @@ -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() { diff --git a/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreDataParser.swift b/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreDataParser.swift index 3b0002d1..abfadf61 100644 --- a/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreDataParser.swift +++ b/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreDataParser.swift @@ -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] - - } - - } - } diff --git a/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreSmoothing.swift b/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreSmoothing.swift new file mode 100644 index 00000000..0d4302fa --- /dev/null +++ b/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreSmoothing.swift @@ -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] + + } + + } + +} diff --git a/xdrip/Constants/ConstantsSmoothing.swift b/xdrip/Constants/ConstantsLibreSmoothing.swift similarity index 88% rename from xdrip/Constants/ConstantsSmoothing.swift rename to xdrip/Constants/ConstantsLibreSmoothing.swift index 4ed3a7db..0831a3a9 100644 --- a/xdrip/Constants/ConstantsSmoothing.swift +++ b/xdrip/Constants/ConstantsLibreSmoothing.swift @@ -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) diff --git a/xdrip/Extensions/Array.swift b/xdrip/Extensions/Array.swift index 08ff82f9..7c1dad0a 100644 --- a/xdrip/Extensions/Array.swift +++ b/xdrip/Extensions/Array.swift @@ -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.., targetRange: Range) 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.., _ b: ArraySlice) -> ArraySlice { - return zip(a,b).map({IsSmoothable(withValue: $0.value * $1.value)})[0..) -> 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, _ ys: ArraySlice) -> (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 } - -} diff --git a/xdrip/Extensions/Double+Smoothable.swift b/xdrip/Extensions/Double+Smoothable.swift index 744e93bf..6f7edfac 100644 --- a/xdrip/Extensions/Double+Smoothable.swift +++ b/xdrip/Extensions/Double+Smoothable.swift @@ -1,6 +1,6 @@ import Foundation -extension Double: Smoothable { +extension Double: SavitzkyGolaySmoothable { var value: Double { get { diff --git a/xdrip/Utilities/KalmanFilter/Double+KalmanFilter.swift b/xdrip/Utilities/KalmanFilter/Double+KalmanFilter.swift new file mode 100644 index 00000000..52a7cd1b --- /dev/null +++ b/xdrip/Utilities/KalmanFilter/Double+KalmanFilter.swift @@ -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 } +} diff --git a/xdrip/Utilities/KalmanFilter/KalmanFilter.swift b/xdrip/Utilities/KalmanFilter/KalmanFilter.swift new file mode 100644 index 00000000..734a1305 --- /dev/null +++ b/xdrip/Utilities/KalmanFilter/KalmanFilter.swift @@ -0,0 +1,61 @@ +/** + Conventional Kalman Filter + */ +public struct KalmanFilter: 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) + } +} diff --git a/xdrip/Utilities/KalmanFilter/KalmanFilterType.swift b/xdrip/Utilities/KalmanFilter/KalmanFilterType.swift new file mode 100644 index 00000000..d8acdb53 --- /dev/null +++ b/xdrip/Utilities/KalmanFilter/KalmanFilterType.swift @@ -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 +} diff --git a/xdrip/Utilities/SavitzkyGolayFilter/SavitzkyGolaySmoothableArray.swift b/xdrip/Utilities/SavitzkyGolayFilter/SavitzkyGolaySmoothableArray.swift new file mode 100644 index 00000000..93a2934d --- /dev/null +++ b/xdrip/Utilities/SavitzkyGolayFilter/SavitzkyGolaySmoothableArray.swift @@ -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.., targetRange: Range) 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.., _ b: ArraySlice) -> ArraySlice { + return zip(a,b).map({IsSmoothable(withValue: $0.value * $1.value)})[0..) -> 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, _ ys: ArraySlice) -> (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 } + +} diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 2fb61ec6..c935a371 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -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