spliner 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,7 +19,7 @@ Spliner requires Ruby 1.9 or later. Install with rubygems:
19
19
  Quick Start
20
20
  -----------
21
21
 
22
- require 'spliner'
22
+ require 'spliner'
23
23
 
24
24
  # Initialize a spline interpolation with x range 0.0..2.0
25
25
  my_spline = Spliner::Spliner.new({0.0 => 0.0, 1.0 => 1.0, 2.0 => 0.5})
@@ -37,6 +37,14 @@ require 'spliner'
37
37
  ex_spline = Spliner::Spliner.new({0.0 => 0.0, 1.0 => 1.0, 2.0 => 0.5}, :extrapolate => '10%', :emethod => :hold)
38
38
  xx = ex_spline[2.1] # returns 0.5
39
39
 
40
+ # Alternative intialization using X and Y arrays
41
+ ar_spline = Spliner::Spliner.new [1.0, 2.0, 3.0], [0.0, 3.0, 1.0]
42
+
43
+ # When duplicate X values are encountered, two or more discontinuous curves are used
44
+ two_spline = Spliner::Spliner.new [1.0, 2.0, 2.0, 3.0], [0.0, 3.0, 0.0, 1.0]
45
+ puts two_spline.sections # prints 2
46
+
47
+
40
48
  Spliner is based on the interpolation described on this page
41
49
  http://en.wikipedia.org/wiki/Spline_interpolation
42
50
 
@@ -48,6 +56,12 @@ Feel free to fork the project on GitHub and send fork requests. Please
48
56
  try to have each feature separated in commits.
49
57
 
50
58
 
59
+ Home page
60
+ ---------
61
+
62
+ http://www.github.com/tallakt/spliner
63
+
64
+ http://rubygems.org/gems/spliner
51
65
 
52
66
  License
53
67
  -------
@@ -1,154 +1,5 @@
1
- #
2
- # Spliner::Spliner
3
- #
4
- require 'matrix'
1
+ require 'spliner/spliner'
5
2
 
6
3
  module Spliner
7
- VERSION = '1.0.1'
8
-
9
- # Spliner::Spliner provides cubic spline interpolation based on provided
10
- # key points on a X-Y curve.
11
- #
12
- # == Example
13
- # require 'spliner'
14
- # # Initialize a spline interpolation with x range 0.0..2.0
15
- # my_spline = Spliner::Spliner.new({0.0 => 0.0, 1.0 => 1.0, 2.0 => 0.5})
16
- # # Perform interpolation on 31 values ranging from 0..2.0
17
- # x_values = (0..30).map {|x| x / 30.0 * 2.0 }
18
- # y_values = x_values.map {|x| my_spline[x] }
19
- #
20
- # http://en.wikipedia.org/wiki/Spline_interpolation
21
- #
22
- class Spliner
23
- attr_reader :range
24
-
25
- # Creates a new Spliner::Spliner object to interpolate between
26
- # the supplied key points. The key points are provided in a hash where
27
- # the key is the X value, and the value is the Y value. The X values
28
- # mush be increasing and not duplicate. You must provide at least
29
- # two values.
30
- #
31
- # options may take the following keys:
32
- #
33
- # :extrapolate
34
- # Specify an area outside the given X values provided that should return
35
- # a valid number. The value may be either a range (eg. -10..110) or a
36
- # percentage value written as a string (eg '10%'). Default is no
37
- # extrapolation.
38
- #
39
- # :emethod
40
- # Specify a method of extrapolation, one of :linear (continue curve as
41
- # a straigt line, default), or :hold (use Y values at the curve endpoints)
42
- #
43
- def initialize(key_points, options = {})
44
-
45
- @points = key_points
46
- @x = @points.keys
47
- @y = @points.values
48
-
49
- check_points_increasing
50
- raise 'Interpolation needs at least two points' unless @points.size >= 2
51
-
52
- @x_pairs = @points.keys.each_cons(2).map {|pair| pair.first..pair.last }
53
-
54
- inv_diff = @x.each_cons(2).map {|x1, x2| 1 / (x2 - x1) }
55
- a_diag = 2.0 * Matrix::diagonal(*vector_helper(inv_diff))
56
- a_non_diag = Matrix::build(@points.size) do |row, col|
57
- if row == col+ 1
58
- inv_diff[col]
59
- elsif col == row + 1
60
- inv_diff[row]
61
- else
62
- 0.0
63
- end
64
- end
65
-
66
- a = a_diag + a_non_diag
67
-
68
- tmp = @points.each_cons(2).map do |p1, p2|
69
- x1, y1 = p1
70
- x2, y2 = p2
71
- 3.0 * (y2 - y1) / (x2 - x1) ** 2.0
72
- end
73
- b = vector_helper(tmp)
74
-
75
- @k = a.inv * b
76
-
77
- options[:extrapolate].tap do |ex|
78
- case ex
79
- when /^\d+(\.\d+)?\s?%$/
80
- percentage = ex[/\d+(\.\d+)?/].to_f
81
- span = @x.last - @x.first
82
- extra = span * percentage * 0.01
83
- @range = (@x.first - extra)..(@x.last + extra)
84
- when Range
85
- @range = ex
86
- when nil
87
- @range = @x.first..@x.last
88
- else
89
- raise 'Unable to use extrapolation parameter'
90
- end
91
- end
92
-
93
- @extrapolation_method = options[:emethod] || :linear
94
- end
95
-
96
- # returns an interpolated value
97
- def get(v)
98
- i = @x_pairs.find_index {|pair| pair.member? v }
99
- if i
100
- dx = @x[i + 1] - @x[i]
101
- dy = @y[i + 1] - @y[i]
102
- t = (v - @x[i]) / dx
103
- a = @k[i] * dx - dy
104
- b = -(@k[i + 1] * dx - dy)
105
- (1 - t) * @y[i] + t * @y[i + 1] + t * (1 - t) * (a * (1 - t) + b * t)
106
- elsif range.member? v
107
- extrapolate(v)
108
- else
109
- nil
110
- end
111
- end
112
-
113
- alias :'[]' :get
114
-
115
-
116
- # for a vector [a, b, c] returns [a, a + b, b + c, c]
117
- # :nodoc:
118
- def vector_helper(a)
119
- Vector[*([0.0] + a)] + Vector[*(a + [0.0])]
120
- end
121
- private :vector_helper
122
-
123
-
124
-
125
- # :nodoc:
126
- def check_points_increasing
127
- @x.each_cons(2) do |x1, x2|
128
- raise 'Points must form a series of x and y values where x is increasing' unless x2 > x1
129
- end
130
- end
131
- private :check_points_increasing
132
-
133
- # :nodoc:
134
- def extrapolate(v)
135
- case @extrapolation_method
136
- when :hold
137
- if v < @x.first
138
- @y.first
139
- else
140
- @y.last
141
- end
142
- else
143
- x, y, k = if v < @x.first
144
- [@x.first, @y.first, @k.first]
145
- else
146
- [@x.last, @y.last, @k[-1]]
147
- end
148
- y + k * (v - x)
149
- end
150
- end
151
- private :extrapolate
152
-
153
- end
4
+ VERSION = '1.0.2'
154
5
  end
@@ -0,0 +1,135 @@
1
+ require 'matrix'
2
+ require 'spliner/spliner_section'
3
+
4
+ module Spliner
5
+ # Spliner::Spliner provides cubic spline interpolation based on provided
6
+ # key points on a X-Y curve.
7
+ #
8
+ # == Example
9
+ #
10
+ # require 'spliner'
11
+ # # Initialize a spline interpolation with x range 0.0..2.0
12
+ # my_spline = Spliner::Spliner.new [0.0, 1.0, 2.0], [0.0, 1.0, 0.5]
13
+ # # Perform interpolation on 31 values ranging from 0..2.0
14
+ # x_values = (0..30).map {|x| x / 30.0 * 2.0 }
15
+ # y_values = x_values.map {|x| my_spline[x] }
16
+ #
17
+ # Algorithm based on http://en.wikipedia.org/wiki/Spline_interpolation
18
+ #
19
+ class Spliner
20
+ attr_reader :range
21
+
22
+ # Creates a new Spliner::Spliner object to interpolate between
23
+ # the supplied key points.
24
+ #
25
+ # The key points shoul be in increaing X order. When duplicate X
26
+ # values are encountered, the spline is split into two or more
27
+ # discontinuous sections.
28
+ #
29
+ # The extrapolation method may be :linear by default, using a linear
30
+ # extrapolation at the curve ends using the curve derivative at the
31
+ # end points. The :hold method will use the Y value at the nearest end
32
+ # point of the curve.
33
+ #
34
+ # @overload initialize(key_points, options)
35
+ # @param key_points [Hash{Float => Float}] keys are X values in increasing order, values Y
36
+ # @param options [Hash]
37
+ # @option options [Range,String] :extrapolate ('0%') either a range or percentage, eg '10.0%'
38
+ # @option options [Symbol] :emethod (:linear) extrapolation method
39
+ #
40
+ # @overload initialize(x, y, options)
41
+ # @param x [Array(Float),Vector] the X values of the key points
42
+ # @param y [Array(Float),Vector] the Y values of the key points
43
+ # @param options [Hash]
44
+ # @option options [Range,String] :extrapolate ('0%') either a range or percentage, eg '10.0%'
45
+ # @option options [Symbol] :emethod (:linear) extrapolation method
46
+ #
47
+ def initialize(*param)
48
+ # sort parameters from two alternative initializer signatures
49
+ x, y = nil
50
+ case param.first
51
+ when Array, Vector
52
+ xx,yy, options = param
53
+ x = xx.to_a
54
+ y = yy.to_a
55
+ else
56
+ points, options = param
57
+ x = points.keys
58
+ y = points.values
59
+ end
60
+ options ||= {}
61
+
62
+ @sections = split_at_duplicates(x).map {|slice| SplinerSection.new x[slice], y[slice] }
63
+
64
+ # Handle extrapolation option parameter
65
+ options[:extrapolate].tap do |ex|
66
+ case ex
67
+ when /^\d+(\.\d+)?\s?%$/
68
+ percentage = ex[/\d+(\.\d+)?/].to_f
69
+ span = x.last - x.first
70
+ extra = span * percentage * 0.01
71
+ @range = (x.first - extra)..(x.last + extra)
72
+ when Range
73
+ @range = ex
74
+ when nil
75
+ @range = x.first..x.last
76
+ else
77
+ raise 'Unable to use extrapolation parameter'
78
+ end
79
+ end
80
+ @extrapolation_method = options[:emethod] || :linear
81
+ end
82
+
83
+ # returns the ranges at each slice between duplicate X values
84
+ def split_at_duplicates(x)
85
+ # find all indices with duplicate x values
86
+ dups = x.each_cons(2).map{|a,b| a== b}.each_with_index.select {|b,i| b }.map {|b,i| i}
87
+ ([-1] + dups + [x.size - 1]).each_cons(2).map {|end0, end1| (end0 + 1)..end1 }
88
+ end
89
+ private :split_at_duplicates
90
+
91
+
92
+ # returns an interpolated value
93
+ def get(v)
94
+ i = @sections.find_index {|section| section.range.member? v }
95
+ if i
96
+ @sections[i].get v
97
+ elsif range.member? v
98
+ extrapolate(v)
99
+ else
100
+ nil
101
+ end
102
+ end
103
+
104
+ alias :'[]' :get
105
+
106
+ # The number of non-continuous sections used
107
+ def sections
108
+ @sections.size
109
+ end
110
+
111
+
112
+
113
+ def extrapolate(v)
114
+ x, y, k = if v < first_x
115
+ [@sections.first.x.first, @sections.first.y.first, @sections.first.k.first]
116
+ else
117
+ [@sections.last.x.last, @sections.last.y.last, @sections.last.k[-1]]
118
+ end
119
+
120
+ case @extrapolation_method
121
+ when :hold
122
+ y
123
+ else
124
+ y + k * (v - x)
125
+ end
126
+ end
127
+ private :extrapolate
128
+
129
+ def first_x
130
+ @sections.first.x.first
131
+ end
132
+ private :first_x
133
+ end
134
+ end
135
+
@@ -0,0 +1,86 @@
1
+ require 'matrix'
2
+ require 'spliner/spliner_section'
3
+
4
+ module Spliner
5
+
6
+ # Spliner::SplinerSection is only used via Spliner::Spliner
7
+ #
8
+ # As the spline algorithm does not handle duplicate X values well, the
9
+ # curve is split into two non continuous parts where duplicate X values
10
+ # appear. Each such part is represented by a SplinerSection
11
+ class SplinerSection
12
+ attr_reader :k, :x, :y
13
+
14
+ def initialize(x, y)
15
+ @x, @y = x, y
16
+ @x_pairs = @x.each_cons(2).map {|pair| pair.first..pair.last }
17
+ check_points_increasing
18
+ calculate_a_k
19
+ end
20
+
21
+ def range
22
+ @x.first..@x.last
23
+ end
24
+
25
+ def calculate_a_k
26
+ if @x.size > 1
27
+ inv_diff = @x.each_cons(2).map {|x1, x2| 1 / (x2 - x1) }
28
+ a_diag = 2.0 * Matrix::diagonal(*vector_helper(inv_diff))
29
+ a_non_diag = Matrix::build(@x.size) do |row, col|
30
+ if row == col+ 1
31
+ inv_diff[col]
32
+ elsif col == row + 1
33
+ inv_diff[row]
34
+ else
35
+ 0.0
36
+ end
37
+ end
38
+
39
+ a = a_diag + a_non_diag
40
+
41
+ tmp = @x.zip(@y).each_cons(2).map do |p1, p2|
42
+ x1, y1 = p1
43
+ x2, y2 = p2
44
+ 3.0 * (y2 - y1) / (x2 - x1) ** 2.0
45
+ end
46
+ b = vector_helper(tmp)
47
+
48
+ @k = a.inv * b
49
+ else
50
+ @k = Vector[0.0]
51
+ end
52
+ end
53
+ private :calculate_a_k
54
+
55
+ # returns an interpolated value
56
+ def get(v)
57
+ i = @x_pairs.find_index {|pair| pair.member? v }
58
+ if i
59
+ dx = @x[i + 1] - @x[i]
60
+ dy = @y[i + 1] - @y[i]
61
+ t = (v - @x[i]) / dx
62
+ a = @k[i] * dx - dy
63
+ b = -(@k[i + 1] * dx - dy)
64
+ (1 - t) * @y[i] + t * @y[i + 1] + t * (1 - t) * (a * (1 - t) + b * t)
65
+ elsif @x.size == 1 && @x.first == v
66
+ @y.first
67
+ else
68
+ nil
69
+ end
70
+ end
71
+
72
+ # for a vector [a, b, c] returns [a, a + b, b + c, c]
73
+ def vector_helper(a)
74
+ Vector[*([0.0] + a)] + Vector[*(a + [0.0])]
75
+ end
76
+ private :vector_helper
77
+
78
+ def check_points_increasing
79
+ @x.each_cons(2) do |x1, x2|
80
+ raise 'Key point\'s X values should be in increasing order' unless x2 > x1
81
+ end
82
+ end
83
+ private :check_points_increasing
84
+ end
85
+ end
86
+
@@ -6,11 +6,38 @@ describe Spliner::Spliner do
6
6
 
7
7
 
8
8
  it 'should not accept x values that are not increasing' do
9
- expect(lambda { Spliner::Spliner.new({0 => 0, 0 => 10})}).to raise_exception
9
+ expect(lambda { Spliner::Spliner.new [0.0, -1.0], [0.0, 1.0] }).to raise_exception
10
10
  end
11
11
 
12
- it 'should not accept less than two values' do
13
- expect(lambda { Spliner::Spliner.new({0 => 0})}).to raise_exception
12
+ it 'should support key points with a single value' do
13
+ s1 = Spliner::Spliner.new Hash[0.0, 0.0]
14
+ expect(s1.get 0.0).to be_within(0.0001).of(0.0)
15
+ expect(s1.get 1.5).to be_nil
16
+
17
+ s2 = Spliner::Spliner.new Hash[0.0, 0.0], :extrapolate => -1..1
18
+ expect(s2.get 0.0).to be_within(0.0001).of(0.0)
19
+ expect(s2.get 0.5).to be_within(0.0001).of(0.0)
20
+ end
21
+
22
+ it 'supports the Hash initializer' do
23
+ s1 = Spliner::Spliner.new Hash[0.0, 0.0, 1.0, 1.0]
24
+ expect(s1[0.5]).to be_within(0.0001).of(0.5)
25
+
26
+ s2 = Spliner::Spliner.new Hash[0.0, 0.0, 1.0, 1.0], :extrapolate => '100%'
27
+ expect(s2[0.5]).to be_within(0.0001).of(0.5)
28
+ expect(s2.range.first).to be_within(0.0001).of(-1.0)
29
+ end
30
+
31
+ it 'supports the x-y array/vector initializer' do
32
+ s1 = Spliner::Spliner.new [0.0, 1.0], [0.0, 1.0]
33
+ expect(s1[0.5]).to be_within(0.0001).of(0.5)
34
+
35
+ s2= Spliner::Spliner.new [0.0, 1.0], [0.0, 1.0], :extrapolate => '100%'
36
+ expect(s2[0.5]).to be_within(0.0001).of(0.5)
37
+ expect(s2.range.first).to be_within(0.0001).of(-1.0)
38
+
39
+ s3 = Spliner::Spliner.new Vector[0.0, 1.0], Vector[0.0, 1.0]
40
+ expect(s3[0.5]).to be_within(0.0001).of(0.5)
14
41
  end
15
42
 
16
43
  it 'should return the data points themselves' do
@@ -71,4 +98,15 @@ describe Spliner::Spliner do
71
98
  expect(s3.range.first).to be_within(0.0001).of(-10.0)
72
99
  expect(s3.range.last).to be_within(0.0001).of(110.0)
73
100
  end
101
+
102
+ it 'splits data points with duplicate X values into separate sections' do
103
+ s = Spliner::Spliner.new [0.0, 1.0, 1.0, 2.0, 2.0, 3.0], [0.0, 0.0, 1.0, 1.0, 2.0, 2.0], :extrapolate => 3.0..4.0
104
+ expect(s.sections).to eq(3)
105
+ expect(s[-1.0]).to be_nil
106
+ expect(s[0.5]).to be_within(0.0001).of(0.0)
107
+ expect(s[1.5]).to be_within(0.0001).of(1.0)
108
+ expect(s[2.5]).to be_within(0.0001).of(2.0)
109
+ expect(s[3.5]).to be_within(0.0001).of(2.0)
110
+ expect(s[5.0]).to be_nil
111
+ end
74
112
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spliner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-21 00:00:00.000000000 Z
12
+ date: 2012-08-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -57,6 +57,8 @@ files:
57
57
  - README.markdown
58
58
  - Rakefile
59
59
  - lib/spliner.rb
60
+ - lib/spliner/spliner.rb
61
+ - lib/spliner/spliner_section.rb
60
62
  - spec/spliner_spec.rb
61
63
  - spliner.gemspec
62
64
  homepage: http://www.github.com/tallakt/spliner
@@ -84,3 +86,4 @@ signing_key:
84
86
  specification_version: 3
85
87
  summary: Cubic spline interpolation library
86
88
  test_files: []
89
+ has_rdoc: