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 +4 -4
- data/README.md +33 -2
- data/lib/savgol.rb +172 -9
- data/lib/savgol/array.rb +5 -54
- data/lib/savgol/version.rb +1 -1
- data/savgol.gemspec +1 -0
- data/spec/savgol_shared_example.rb +1 -8
- data/spec/savgol_spec.rb +83 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc7a6227404449143b6b264e85308bda89e5e2fa
|
4
|
+
data.tar.gz: 5e4089173d7d618e2bfe372a5758e78c6be22f36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
##
|
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
|
data/lib/savgol.rb
CHANGED
@@ -1,13 +1,176 @@
|
|
1
1
|
require 'savgol/version'
|
2
|
-
require '
|
2
|
+
require 'matrix'
|
3
3
|
|
4
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
data/lib/savgol/array.rb
CHANGED
@@ -1,57 +1,8 @@
|
|
1
|
-
require
|
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
|
-
|
4
|
+
# see Savgol#savgol
|
5
|
+
def savgol(*args)
|
6
|
+
Savgol.savgol(self, *args)
|
7
|
+
end
|
57
8
|
end
|
data/lib/savgol/version.rb
CHANGED
data/savgol.gemspec
CHANGED
@@ -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
|
-
|
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
|
data/spec/savgol_spec.rb
ADDED
@@ -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.
|
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-
|
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
|