savgol 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 74cb6ba4eb41d56918d8e0994ecab490234bae85
4
- data.tar.gz: 60e5ce50c918ac33ccf028a9e26773d06d59e7af
3
+ metadata.gz: bc7a6227404449143b6b264e85308bda89e5e2fa
4
+ data.tar.gz: 5e4089173d7d618e2bfe372a5758e78c6be22f36
5
5
  SHA512:
6
- metadata.gz: 9698cb0c54a48b474f27badfa100b65ca92857829f59ed38ed304c5e9ed4428e28dbf260c6829731c5dbef197279d4796212d0ce9ece1252cbaa809a1fb0730e
7
- data.tar.gz: cabf344682dbe30bd2d1beb1209dbdbf7530fcadfd70079fcd5edb967fcbd41bce049235c7d2f9482e10d007bd435f3afe2f387b5390163940a97cfe748c8850
6
+ metadata.gz: 6552c4e22998e3f8fb8b594f89684c42fe296496e1481b7f03aef6d43d68032ba23a8341a8288e2695b73b7e5527fc382f92dba9a02ff84764395949735c1836
7
+ data.tar.gz: dab124a5685cd5c3215d9b3ab628d626ec44590e883c1d340d4bbcad278d7a74f426520abdd19a8cbc16ee2dddd652014ecc1001a6ff7dcf9279c65ce91ce763
data/README.md CHANGED
@@ -2,17 +2,48 @@
2
2
 
3
3
  Provides implementations of Savitzky-Golay smoothing (filtering).
4
4
 
5
- The gem is based on the [scipy implementation](http://www.scipy.org/Cookbook/SavitzkyGolay). A good explanation of the process may be found [here on stackexchange](http://dsp.stackexchange.com/a/9494).
5
+ The gem is based on the [scipy implementation](http://www.scipy.org/Cookbook/SavitzkyGolay) (gives exactly the same result). A good explanation of the process may be found [here on stackexchange](http://dsp.stackexchange.com/a/9494).
6
6
 
7
7
  # Examples
8
8
 
9
- ## Array implementation
9
+ ## Evenly spaced data
10
+
11
+ ### Array implementation (object oriented)
10
12
 
11
13
  require 'savgol/array'
12
14
  data = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
13
15
  # window size = 5, polynomial order = 3
14
16
  data.savgol(5,3)
15
17
 
18
+ ### Module implementation
19
+
20
+ require 'savgol'
21
+ data = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
22
+ # window size = 5, polynomial order = 3
23
+ Savgol.savgol(data, 5,3)
24
+
25
+ ## Uneven data
26
+
27
+ The speed gain of the Savitzky-Golay filter is lost when interpolating
28
+ unevenly spaced data and devolves into simple polynomial linear regression [At
29
+ least I believe they are equivalent in complexity since both require a matrix
30
+ pseudo-inverse calculation as the most complex operation]. Even though it may
31
+ be much slower, a filter that handles unevenly spaced data may be useful in
32
+ some cases.
33
+
34
+ require 'savgol'
35
+ xvals = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
36
+ yvals = %w(-2 1 0 1 1 3 4 7 8 9 7 4 1 2).map {|v| v.to_f + 30 }
37
+
38
+ ### Interpolate at the given x-values
39
+
40
+ Savgol.savgol_uneven(xvals, yvals, 5, 2)
41
+
42
+ ### Interpolate at a new set of x values
43
+
44
+ new_xvals = xvals.map {|v| v + 0.5 }
45
+ Savgol.savgol_uneven(xvals, yvals, 5, 2, new_xvals: new_xvals)
46
+
16
47
  # Installation
17
48
 
18
49
  gem install savgol
@@ -1,13 +1,176 @@
1
1
  require 'savgol/version'
2
- require 'savgol/array'
2
+ require 'matrix'
3
3
 
4
- module Savgol
4
+ class NilEnumerator < Enumerator
5
+ def initialize(enum)
6
+ @enum = enum
7
+ end
8
+
9
+ def next
10
+ begin
11
+ @enum.next
12
+ rescue StopIteration
13
+ nil
14
+ end
15
+ end
5
16
  end
17
+
18
+ module Savgol
19
+ class << self
20
+ # Does simple least squares to fit a polynomial based on the given x
21
+ # values. The major speed-boost of doing savgol is lost for uneven time points.
22
+ # TODO: implement for different derivatives; implement with a window that
23
+ # is of fixed size (not based on the number of points)
24
+ def savgol_uneven(xvals, yvals, window_points=11, order=4, check_args: false, new_xvals: nil)
25
+ sg_check_arguments(window_points, order) if check_args
26
+
27
+ half_window = (window_points -1) / 2
28
+ yvals_padded = sg_pad_ends(yvals, half_window)
29
+ xvals_padded = sg_pad_xvals(xvals, half_window)
30
+ xvals_size = xvals.size
31
+
32
+ if new_xvals
33
+ nearest_xval_indices = sg_nearest_index(xvals, new_xvals)
34
+ new_xvals.zip(nearest_xval_indices).map do |new_xval, index|
35
+ sg_regress_and_find(
36
+ xvals_padded[index,window_points],
37
+ yvals_padded[index,window_points],
38
+ order,
39
+ new_xval
40
+ )
41
+ end
42
+ else
43
+ xs_iter = xvals_padded.each_cons(window_points)
44
+ yvals_padded.each_cons(window_points).map do |ys|
45
+ xs = xs_iter.next
46
+ sg_regress_and_find(xs, ys, order, xs[half_window])
47
+ end
48
+ end
49
+ end
50
+
51
+ # returns the nearest index in original_vals for each value in new_vals
52
+ # (assumes both are sorted arrays). complexity: O(n + m)
53
+ def sg_nearest_index(original_vals, new_vals)
54
+ newval_iter = NilEnumerator.new(new_vals.each)
55
+ indices = []
56
+
57
+ index_iter = NilEnumerator.new((0...original_vals.size).each)
58
+ index = index_iter.next
59
+ newval=newval_iter.next
60
+ last_index = original_vals.size-1
61
+
62
+ until newval >= original_vals[index]
63
+ indices << index
64
+ break unless newval = newval_iter.next
65
+ end
66
+
67
+ loop do
68
+ break unless newval
69
+
70
+ if index.nil?
71
+ indices << last_index
72
+ else
73
+ until newval <= original_vals[index]
74
+ index = index_iter.next
75
+ if !index
76
+ indices << last_index
77
+ break
78
+ end
79
+ end
80
+
81
+ if index
82
+ if newval < original_vals[index]
83
+ indices << ((newval - original_vals[index-1]) <= (original_vals[index] - newval) ? index-1 : index)
84
+ else
85
+ indices << index
86
+ end
87
+ end
88
+ end
89
+ newval = newval_iter.next
90
+ end
91
+
92
+ indices
93
+ end
6
94
 
7
- # ar = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
8
- # numpy_savgol_output = [1.0, 2.0, 3.12857143, 3.57142857, 4.27142857, 4.12571429, 3.36857143, 2.69714286, 2.04, 0.32571429, -0.05714286, 0.8, 0.51428571, -2.17142857, -5.25714286, -7.65714286, -6.4, -2.77142857, 0.17142857, 0.91428571, 1.0]
9
- # sg = ar.savgol(5,3)
10
- #
11
- # sg.zip(numpy_savgol_output) do |sgv, numpy_sgv|
12
- # p "#{sgv} vs #{numpy_sgv} diff #{(sgv - numpy_sgv).abs.round(8)}"
13
- # end
95
+ def sg_regress_and_find(xdata, ydata, order, xval)
96
+ xdata_matrix = xdata.map { |xi| (0..order).map { |pow| (xi**pow).to_f } }
97
+ mx = Matrix[*xdata_matrix]
98
+ my = Matrix.column_vector(ydata)
99
+ ((mx.t * mx).inv * mx.t * my).transpose.to_a[0].
100
+ zip((0..order).to_a).
101
+ map {|coeff, pow| coeff * (xval**pow) }.
102
+ reduce(:+)
103
+ end
104
+
105
+ def savgol(array, window_points=11, order=4, deriv: 0, check_args: false)
106
+ sg_check_arguments(window_points, order) if check_args
107
+ half_window = (window_points -1) / 2
108
+ weights = sg_weights(half_window, order, deriv)
109
+ padded_array = sg_pad_ends(array, half_window)
110
+ sg_convolve(padded_array, weights)
111
+ end
112
+
113
+ def sg_check_arguments(window_points, order)
114
+ if !window_points.is_a?(Integer) || window_points.abs != window_points || window_points % 2 != 1 || window_points < 1
115
+ raise ArgumentError, "window_points size must be a positive odd integer"
116
+ end
117
+ if !order.is_a?(Integer) || order < 0
118
+ raise ArgumentError, "order must be an integer >= 0"
119
+ end
120
+ if window_points < order + 2
121
+ raise ArgumentError, "window_points is too small for the polynomials order"
122
+ end
123
+ end
124
+
125
+ def sg_convolve(data, weights, mode=:valid)
126
+ data.each_cons(weights.size).map do |ar|
127
+ ar.zip(weights).map {|pair| pair[0] * pair[1] }.reduce(:+)
128
+ end
129
+ end
130
+
131
+ # pads the ends with the reverse, geometric inverse sequence
132
+ def sg_pad_ends(array, half_window)
133
+ start = array[1..half_window]
134
+ start.reverse!
135
+ start.map! {|v| array[0] - (v - array[0]).abs }
136
+
137
+ fin = array[(-half_window-1)...-1]
138
+ fin.reverse!
139
+ fin.map! {|v| array[-1] + (v - array[-1]).abs }
140
+ start.push(*array, *fin)
141
+ end
142
+
143
+ # pads the ends of x vals
144
+ def sg_pad_xvals(array, half_window)
145
+ deltas = array[0..half_window].each_cons(2).map {|a,b| b-a }
146
+ start = array[0]
147
+ prevals = deltas.map do |delta|
148
+ newval = start - delta
149
+ start = newval
150
+ newval
151
+ end
152
+ prevals.reverse!
153
+
154
+ deltas = array[(-half_window-1)..-1].each_cons(2).map {|a,b| b-a }
155
+ start = array[-1]
156
+ postvals = deltas.reverse.map do |delta|
157
+ newval = start + delta
158
+ start = newval
159
+ newval
160
+ end
161
+
162
+ prevals.push(*array, *postvals)
163
+ end
164
+
165
+ # returns an object that will convolve with the padded array
166
+ def sg_weights(half_window, order, deriv=0)
167
+ # byebug
168
+ mat = Matrix[ *(-half_window..half_window).map {|k| (0..order).map {|i| k**i }} ]
169
+ # Moore-Penrose psuedo-inverse without SVD (not so precize)
170
+ # A' = (A.t * A)^-1 * A.t
171
+ pinv_matrix = Matrix[*(mat.transpose*mat).to_a].inverse * Matrix[*mat.to_a].transpose
172
+ pinv = Matrix[*pinv_matrix.to_a]
173
+ pinv.row(deriv).to_a
174
+ end
175
+ end
176
+ end
@@ -1,57 +1,8 @@
1
- require "matrix"
2
-
3
- module Savgol
4
-
5
- def savgol(window_size, order, deriv=0, check_args=false)
6
-
7
- # check arguments
8
- if !window_size.is_a?(Integer) || window_size.abs != window_size || window_size % 2 != 1 || window_size < 1
9
- raise ArgumentError, "window_size size must be a positive odd integer"
10
- end
11
- if !order.is_a?(Integer) || order < 0
12
- raise ArgumentError, "order must be an integer >= 0"
13
- end
14
- if window_size < order + 2
15
- raise ArgumentError, "window_size is too small for the polynomials order"
16
- end
17
-
18
- half_window = (window_size -1) / 2
19
- weights = sg_weights(half_window, order, deriv)
20
- ar = sg_pad_ends(half_window)
21
- sg_convolve(ar, weights)
22
- end
23
-
24
- def sg_convolve(data, weights, mode=:valid)
25
- data.each_cons(weights.size).map do |ar|
26
- ar.zip(weights).map {|pair| pair[0] * pair[1] }.reduce(:+)
27
- end
28
- end
29
-
30
- # pads the ends with the reverse, geometric inverse sequence
31
- def sg_pad_ends(half_window)
32
- start = self[1..half_window]
33
- start.reverse!
34
- start.map! {|v| self[0] - (v - self[0]).abs }
35
-
36
- fin = self[(-half_window-1)...-1]
37
- fin.reverse!
38
- fin.map! {|v| self[-1] + (v - self[-1]).abs }
39
- start.push(*self, *fin)
40
- end
41
-
42
- # returns an object that will convolve with the padded array
43
- def sg_weights(half_window, order, deriv=0)
44
- # byebug
45
- mat = Matrix[ *(-half_window..half_window).map {|k| (0..order).map {|i| k**i }} ]
46
- # Moore-Penrose psuedo-inverse without SVD (not so precize)
47
- # A' = (A.t * A)^-1 * A.t
48
- pinv_matrix = Matrix[*(mat.transpose*mat).to_a].inverse * Matrix[*mat.to_a].transpose
49
- pinv = Matrix[*pinv_matrix.to_a]
50
- pinv.row(deriv).to_a
51
- end
52
-
53
- end
1
+ require 'savgol'
54
2
 
55
3
  class Array
56
- include Savgol
4
+ # see Savgol#savgol
5
+ def savgol(*args)
6
+ Savgol.savgol(self, *args)
7
+ end
57
8
  end
@@ -1,3 +1,3 @@
1
1
  module Savgol
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  ["rspec", "~> 2.14.1"],
31
31
  ["rdoc", "~> 4.1.0"],
32
32
  ["simplecov", "~> 0.8.2"],
33
+ ["gnuplot"],
33
34
  ].each do |args|
34
35
  spec.add_development_dependency(*args)
35
36
  end
@@ -5,16 +5,9 @@ shared_examples "a savgol smoother" do
5
5
  object
6
6
  end
7
7
 
8
- it 'pads with the reverse geometrically inverted sequence' do
9
- expect(smoother.sg_pad_ends(2)).to eq [-1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2]
10
- expect(smoother.sg_pad_ends(3)).to eq [-2, -1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2, 4]
11
- end
12
-
13
8
  describe 'smoothing a signal' do
14
9
  let(:smoother) do
15
- object = described_class[1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
16
- object.extend(Savgol)
17
- object
10
+ described_class[1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
18
11
  end
19
12
 
20
13
  it "works for the simple case" do
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ describe Savgol do
4
+ describe 'padding' do
5
+ let(:array) { [1, 2, 3, 4, -7, -2, 0, 1, 1] }
6
+ it 'pads with the reverse geometrically inverted sequence' do
7
+ expect(Savgol.sg_pad_ends(array, 2)).to eq [-1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2]
8
+ expect(Savgol.sg_pad_ends(array, 3)).to eq [-2, -1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2, 4]
9
+ end
10
+
11
+ it 'pads unevenly spaced x vals' do
12
+ ar = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
13
+ expect(Savgol.sg_pad_xvals(ar, 2)).to eq %w(-4 -2 -1 0 2 3 4 7 8 10 11 12 13 14 17 18 19 22]).map(&:to_f)
14
+ end
15
+ end
16
+
17
+ describe 'finding nearest index' do
18
+ let(:original) { [0,1,2,3,4] }
19
+
20
+ it 'finds nearest index to the left' do
21
+ newvals = [-2, -1]
22
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([0,0])
23
+ end
24
+
25
+ it 'finds nearest indices in the middle' do
26
+ newvals = [1,1.4,1.5,1.6,2,2.1]
27
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([1,1,1,2,2,2])
28
+ end
29
+
30
+ it 'finds nearest index to the right' do
31
+ newvals = [5,6]
32
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([4,4])
33
+ end
34
+
35
+ it 'efficiently finds the nearest index' do
36
+ newvals = [-2,-1,1,2.4,2.5,4,4.5,5]
37
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([0,0,1,2,2,4,4,4])
38
+ end
39
+ end
40
+
41
+ describe 'smoothing unevenly spaced data' do
42
+ xvals = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
43
+ new_xvals = xvals.map {|v| v + 0.5 }
44
+ yvals = %w(-2 1 0 1 1 3 4 7 8 9 7 4 1 2).map {|v| v.to_f + 30 }
45
+
46
+ yvals_on = Savgol.savgol_uneven(xvals, yvals, 5, 2)
47
+
48
+ yvals_off = Savgol.savgol_uneven(xvals, yvals, 5, 2, new_xvals: new_xvals)
49
+
50
+ yvals_pretend_even = Savgol.savgol(yvals, 5, 2)
51
+
52
+ if false
53
+ require 'gnuplot'
54
+ Gnuplot.open do |gp|
55
+ Gnuplot::Plot.new(gp) do |plot|
56
+
57
+ plot.data << Gnuplot::DataSet.new([new_xvals, yvals_off]) do |ds|
58
+ ds.title = "x interpolated"
59
+ ds.with = "linespoints"
60
+ end
61
+
62
+ plot.data << Gnuplot::DataSet.new([xvals, yvals_pretend_even]) do |ds|
63
+ ds.title = "pretend evenly spaced"
64
+ ds.with = "linespoints"
65
+ end
66
+
67
+ plot.data << Gnuplot::DataSet.new([xvals, yvals_on]) do |ds|
68
+ ds.title = "smoothed"
69
+ ds.with = "linespoints"
70
+ end
71
+
72
+ plot.data << Gnuplot::DataSet.new([xvals, yvals]) do |ds|
73
+ ds.title = "original"
74
+ ds.with = "points"
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: savgol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John T. Prince
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-03-27 00:00:00.000000000 Z
12
+ date: 2014-04-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -81,6 +81,20 @@ dependencies:
81
81
  - - ~>
82
82
  - !ruby/object:Gem::Version
83
83
  version: 0.8.2
84
+ - !ruby/object:Gem::Dependency
85
+ name: gnuplot
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
84
98
  description: Extends Array class with method which calculates applies Savitzky-Golay
85
99
  filter used for smoothing the data
86
100
  email:
@@ -102,6 +116,7 @@ files:
102
116
  - savgol.gemspec
103
117
  - spec/savgol/array_spec.rb
104
118
  - spec/savgol_shared_example.rb
119
+ - spec/savgol_spec.rb
105
120
  - spec/spec_helper.rb
106
121
  homepage: http://github.com/princelab/savgol
107
122
  licenses:
@@ -130,4 +145,5 @@ summary: performs Savitzky-Golay smoothing
130
145
  test_files:
131
146
  - spec/savgol/array_spec.rb
132
147
  - spec/savgol_shared_example.rb
148
+ - spec/savgol_spec.rb
133
149
  - spec/spec_helper.rb