spliner 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: