SavitzkyGolayFilter.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import Foundation
  2. /// allowed values are 0, 1, 2 or 3. It's the index in coefficients
  3. private var coefficientsRowToUse = 3
  4. /// Savitzky Golay coefficients
  5. private let coefficients = [
  6. [-3.0, 12.0, 17.0, 12.0, -3.0],
  7. [-2.0, 3.0, 6.0, 7.0, 6.0, 3.0, -2.0],
  8. [-21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0],
  9. [-36.0, 9.0, 44.0, 69.0, 84.0, 89.0, 84.0, 69.0, 44.0, 9.0, -36.0]
  10. ]
  11. /// an array with elements of a type that conforms to Smoothable, can be filtered using the Savitzky Golay algorithm
  12. protocol SavitzkyGolaySmoothable {
  13. /// value to be smoothed
  14. var value: Double { get set }
  15. }
  16. /// local help class
  17. private class IsSmoothable: SavitzkyGolaySmoothable {
  18. var value: Double = 0.0
  19. init(withValue value: Double = 0.0) {
  20. self.value = value
  21. }
  22. }
  23. extension Array where Element: SavitzkyGolaySmoothable {
  24. /// - apply Savitzky Golay filter
  25. /// - 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
  26. /// - ...continue with 5 here in the explanation ...
  27. /// - 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
  28. /// - 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
  29. /// see also example https://github.com/JohanDegraeve/xdripswift/wiki/Libre-value-smoothing
  30. mutating func smoothSavitzkyGolayQuaDratic(withFilterWidth filterWidth: Int = 5) {
  31. // filterWidthToUse is the value of filterWidth to use in the algorithm. By default filterWidthToUse = parameter value filterWidth
  32. var filterWidthToUse = filterWidth
  33. // calculate coefficientsRowToUse based on filterWdith
  34. switch filterWidth {
  35. case 5:
  36. coefficientsRowToUse = 3
  37. case 4:
  38. coefficientsRowToUse = 2
  39. case 3:
  40. coefficientsRowToUse = 1
  41. case 2:
  42. coefficientsRowToUse = 0
  43. default:
  44. // invalid filterWidth was given in parameterList, use default value
  45. coefficientsRowToUse = 3
  46. filterWidthToUse = 5
  47. }
  48. // using 5 here in the comments as value for filterWidthToUse
  49. // the amount of elements must be at least 5. If that's not the case then don't apply any smoothing
  50. guard count >= filterWidthToUse else { return }
  51. // 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)
  52. // the 5 elements will be estimated by doing linear regression of the first 5 and last 5 elements of the original input array respectively
  53. // 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
  54. var tempArray = [SavitzkyGolaySmoothable]()
  55. for element in self {
  56. tempArray.append(element)
  57. }
  58. // now prepend and append with 5 elements, each with a default value 0.0
  59. for _ in 0 ..< filterWidthToUse {
  60. tempArray.insert(IsSmoothable(), at: 0)
  61. tempArray.append(IsSmoothable())
  62. }
  63. // so now we have tempArray, of length size of original array + 2 * 5
  64. // the first 5 and the last 5 elements are of type IsSmoothable with value 0
  65. // - indicesArray is a help array needed for the function linearRegressionCreator
  66. // - 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
  67. // - we give each IsSmoothable the value of the index, meaning from 0 up to (length of tempArray) - 1
  68. // - in fact it's not really smoothable, it's just because we use isSmoothable in function linearRegressionCreator
  69. var indicesArray = [SavitzkyGolaySmoothable]()
  70. for index in 0 ..< (count + (filterWidthToUse * 2)) {
  71. indicesArray.append(IsSmoothable(withValue: Double(index)))
  72. }
  73. /// - 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
  74. /// - 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
  75. /// - then executes the regression for every element in the range defined by targetRange, again in tempArray
  76. let doRegression = { (predictorRange: Range<Int>, targetRange: Range<Int>) in
  77. // calculate the linearRegression function
  78. let linearRegression = linearRegressionCreator(indicesArray[predictorRange], tempArray[predictorRange])
  79. // ready to do the linear regression for the targetRange in tempArray
  80. for index in targetRange {
  81. tempArray[index].value = linearRegression(indicesArray[index].value)
  82. }
  83. }
  84. // now do the regression for the 5 first elements
  85. doRegression(filterWidthToUse ..< (filterWidthToUse * 2), 0 ..< filterWidthToUse)
  86. // now do the regression for the 5 last elements
  87. doRegression(
  88. (tempArray.count - filterWidthToUse * 2) ..< (tempArray.count - filterWidthToUse),
  89. (tempArray.count - filterWidthToUse) ..< tempArray.count
  90. )
  91. // now start filtering
  92. // initialize array that will hold the resulting filtered values
  93. var filteredValues = [Double]()
  94. // calculate divider
  95. let divider = coefficients[coefficientsRowToUse].reduce(0, { x, y in
  96. x + y
  97. })
  98. // filter each original value
  99. for _ in 0 ..< count {
  100. // add a new element to filteredValues, start value is 0.0
  101. // this new value will be the last element, so we access it with index filteredValues.count - 1
  102. filteredValues.append(0.0)
  103. // iterate through the coefficients
  104. for (index, coefficient) in coefficients[coefficientsRowToUse].enumerated() {
  105. filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] + coefficient *
  106. tempArray[index + filteredValues.count - 1].value
  107. }
  108. filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] / divider
  109. }
  110. // now assign the new values to the original objects
  111. for (index, _) in enumerated() {
  112. self[index].value = filteredValues[index]
  113. }
  114. }
  115. }
  116. /// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
  117. private func multiply(
  118. _ a: ArraySlice<SavitzkyGolaySmoothable>,
  119. _ b: ArraySlice<SavitzkyGolaySmoothable>
  120. ) -> ArraySlice<SavitzkyGolaySmoothable> {
  121. zip(a, b).map({ IsSmoothable(withValue: $0.value * $1.value) })[0 ..< a.count]
  122. }
  123. /// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
  124. private func average(_ input: ArraySlice<SavitzkyGolaySmoothable>) -> Double {
  125. (input.reduce(IsSmoothable(), { (x: SavitzkyGolaySmoothable, y: SavitzkyGolaySmoothable) in
  126. IsSmoothable(withValue: x.value + y.value) })).value / Double(input.count)
  127. }
  128. /// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
  129. private func linearRegressionCreator(
  130. _ xs: ArraySlice<SavitzkyGolaySmoothable>,
  131. _ ys: ArraySlice<SavitzkyGolaySmoothable>
  132. ) -> (Double) -> Double {
  133. let sum1 = average(multiply(ys, xs)) - average(xs) * average(ys)
  134. let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
  135. let slope = sum1 / sum2
  136. let intercept = average(ys) - slope * average(xs)
  137. return { x in intercept + slope * x }
  138. }