roctave 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +33 -0
  4. data/ext/roctave/cu8_file_reader.c +331 -0
  5. data/ext/roctave/cu8_file_reader.h +30 -0
  6. data/ext/roctave/extconf.rb +6 -0
  7. data/ext/roctave/fir_filter.c +795 -0
  8. data/ext/roctave/fir_filter.h +29 -0
  9. data/ext/roctave/freq_shifter.c +410 -0
  10. data/ext/roctave/freq_shifter.h +29 -0
  11. data/ext/roctave/iir_filter.c +462 -0
  12. data/ext/roctave/iir_filter.h +29 -0
  13. data/ext/roctave/roctave.c +38 -0
  14. data/ext/roctave/roctave.h +27 -0
  15. data/lib/roctave.rb +168 -0
  16. data/lib/roctave/bilinear.rb +92 -0
  17. data/lib/roctave/butter.rb +87 -0
  18. data/lib/roctave/cheby.rb +180 -0
  19. data/lib/roctave/cu8_file_reader.rb +45 -0
  20. data/lib/roctave/dft.rb +280 -0
  21. data/lib/roctave/filter.rb +64 -0
  22. data/lib/roctave/finite_difference_coefficients.rb +73 -0
  23. data/lib/roctave/fir.rb +121 -0
  24. data/lib/roctave/fir1.rb +134 -0
  25. data/lib/roctave/fir2.rb +246 -0
  26. data/lib/roctave/fir_design.rb +311 -0
  27. data/lib/roctave/firls.rb +380 -0
  28. data/lib/roctave/firpm.rb +499 -0
  29. data/lib/roctave/freq_shifter.rb +47 -0
  30. data/lib/roctave/freqz.rb +233 -0
  31. data/lib/roctave/iir.rb +80 -0
  32. data/lib/roctave/interp1.rb +78 -0
  33. data/lib/roctave/plot.rb +748 -0
  34. data/lib/roctave/poly.rb +46 -0
  35. data/lib/roctave/roots.rb +73 -0
  36. data/lib/roctave/sftrans.rb +157 -0
  37. data/lib/roctave/version.rb +3 -0
  38. data/lib/roctave/window.rb +116 -0
  39. data/roctave.gemspec +79 -0
  40. data/samples/butter.rb +12 -0
  41. data/samples/cheby.rb +28 -0
  42. data/samples/dft.rb +18 -0
  43. data/samples/differentiator.rb +48 -0
  44. data/samples/differentiator_frequency_scaling.rb +52 -0
  45. data/samples/fft.rb +40 -0
  46. data/samples/finite_difference_coefficient.rb +53 -0
  47. data/samples/fir1.rb +13 -0
  48. data/samples/fir2.rb +14 -0
  49. data/samples/fir2_windows.rb +29 -0
  50. data/samples/fir2bank.rb +30 -0
  51. data/samples/fir_low_pass.rb +44 -0
  52. data/samples/firls.rb +77 -0
  53. data/samples/firpm.rb +78 -0
  54. data/samples/hilbert_transformer.rb +20 -0
  55. data/samples/hilbert_transformer_frequency_scaling.rb +47 -0
  56. data/samples/plot.rb +45 -0
  57. data/samples/stem.rb +8 -0
  58. data/samples/type1.rb +25 -0
  59. data/samples/type3.rb +24 -0
  60. data/samples/windows.rb +25 -0
  61. metadata +123 -0
@@ -0,0 +1,47 @@
1
+ ## Copyright (C) 2019 Théotime Bollengier <theotime.bollengier@gmail.com>
2
+ ##
3
+ ## This file is part of Roctave
4
+ ##
5
+ ## Roctave is free software: you can redistribute it and/or modify
6
+ ## it under the terms of the GNU General Public License as published by
7
+ ## the Free Software Foundation, either version 3 of the License, or
8
+ ## (at your option) any later version.
9
+ ##
10
+ ## Roctave is distributed in the hope that it will be useful,
11
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ ## GNU General Public License for more details.
14
+ ##
15
+ ## You should have received a copy of the GNU General Public License
16
+ ## along with Roctave. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+
19
+ module Roctave
20
+ class FreqShifter
21
+
22
+ # Multiply the signal with exp(2*i*pi*f*t)
23
+ # @overload shift(signal, freq: 0.5, fs: 1.0, initial_phase: 0.0)
24
+ # @param signal [Array<Float,Complex>, Float, Complex] a single or and array of float or complex numbers
25
+ # @param freq [Float] the amount of frequency to shift
26
+ # @param fs [Float] the sampling frequency
27
+ # @param initial_phase [Float] the initial phase of the oscillator
28
+ # @return [Array<Float,Complex>, Float, Complex] a single or and array of float or complex numbers
29
+ def self.shift (sig, *args)
30
+ shifter = Roctave::FreqShifter.new(*args)
31
+ shifter.shift sig
32
+ end
33
+
34
+ # Multiply the signal with cos(2*i*pi*f*t)
35
+ # @overload shift(signal, freq: 0.5, fs: 1.0, initial_phase: 0.0)
36
+ # @param signal [Array<Float,Complex>, Float, Complex] a single or and array of float or complex numbers
37
+ # @param freq [Float] the amount of frequency to shift
38
+ # @param fs [Float] the sampling frequency
39
+ # @param initial_phase [Float] the initial phase of the oscillator
40
+ # @return [Array<Float,Complex>, Float, Complex] a single or and array of float or complex numbers
41
+ def self.amplitude_modulate (sig, *args)
42
+ shifter = Roctave::FreqShifter.new(*args)
43
+ shifter.amplitude_modulate sig
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,233 @@
1
+ ## Copyright (C) 1994-2015 John W. Eaton (GNU Octave implementation)
2
+ ## Copyright (C) 2019 Théotime Bollengier <theotime.bollengier@gmail.com>
3
+ ##
4
+ ## This file is part of Roctave
5
+ ##
6
+ ## Roctave is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## Roctave is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with Roctave. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+
20
+ module Roctave
21
+
22
+ # @!group Plotting
23
+
24
+ ##
25
+ # Return the complex frequency response H of the rational IIR filter
26
+ # whose numerator and denominator coefficients are B and A,
27
+ # respectively.
28
+ #
29
+ # If A is omitted, the denominator is assumed to be 1 (this
30
+ # corresponds to a simple FIR filter).
31
+ #
32
+ # @param b [Array<Numeric>] Numerator of the IIR filter.
33
+ # @param a [Array<Numeric>] Denominator of the IIR filter.
34
+ # @param nb_points [Integer] Number of points on which to evaluate the response. Faster if power of two.
35
+ # @param region [Symbol] Either :half of :whole
36
+ # @param fs [Float] If not specified, output frequencies are on [0 pi] or [-pi pi] depending on +region+
37
+ # @param opts [Symbols] You can specify :magnitude, :phase and :group_delay to plot the magnitude, phase and group delay response, and specify :degree and :dB, and :shift for fftshifting when ploting the whole region
38
+ def self.freqz (b, *opts, nb_points: 512, region: nil, fs: nil)
39
+ nb_points = [4, nb_points.to_i].max
40
+ fs = fs.to_f.abs if fs
41
+ sampling_rate = fs
42
+ normalize_freqency = fs.nil?
43
+ degree = false
44
+ log = false
45
+ plot_m = false
46
+ plot_p = false
47
+ plot_d = false
48
+ shift = false
49
+ a = [1.0]
50
+ opts.each do |opt|
51
+ case opt
52
+ when :magnitude
53
+ plot_m = true
54
+ when :phase
55
+ plot_p = true
56
+ when :group_delay
57
+ plot_d = true
58
+ when :degree
59
+ degree = true
60
+ when :dB
61
+ log = true
62
+ when :shift
63
+ shift = true
64
+ when Array
65
+ a = opt
66
+ else
67
+ raise ArgumentError.new "Unexpected argument \"#{opt}\""
68
+ end
69
+ end
70
+ do_plot = (plot_m or plot_p)
71
+ raise ArgumentError.new "First argument must be an array" unless b.kind_of?(Array)
72
+ b = [1.0] if b.empty?
73
+ if region != :half and region != :whole then
74
+ if b.find{|v| v.kind_of?(Complex)} or a.find{|v| v.kind_of?(Complex)} then
75
+ region = :whole
76
+ else
77
+ region = :half
78
+ end
79
+ end
80
+ if fs.nil? then
81
+ fs = 2*Math::PI
82
+ end
83
+
84
+ k = [b.length, a.length].max
85
+ if k > nb_points/2 and plot_p then
86
+ ## Ensure a causal phase response.
87
+ nb_points = nb_points * 2**Math.log2(2.0*k/nb_points).ceil
88
+ end
89
+
90
+ if region == :whole then
91
+ ####
92
+ =begin
93
+ n = nb_points
94
+ if do_plot then
95
+ f = (0..nb_points).collect{|i| fs * i / n} # do 1 more for the plot
96
+ else
97
+ f = (0...nb_points).collect{|i| fs * i / n}
98
+ end
99
+ =end
100
+ ####
101
+ n = nb_points
102
+ f = (0...nb_points).collect{|i| fs * i / n}
103
+ ####
104
+ else
105
+ n = 2*nb_points
106
+ #nb_points += 1 if do_plot
107
+ f = (0...nb_points).collect{|i| fs * i / n}
108
+ end
109
+
110
+ pad_sz = n*(k.to_f / n).ceil
111
+ b = Roctave.postpad(b, pad_sz)
112
+ a = Roctave.postpad(a, pad_sz)
113
+
114
+ hb = Roctave.zeros(nb_points)
115
+ ha = Roctave.zeros(nb_points)
116
+
117
+ (0...pad_sz).step(n).each do |i|
118
+ tmp = Roctave.dft(Roctave.postpad(b[i ... i+n], n))[0...nb_points]
119
+ hb = hb.collect.with_index{|v, j| v+tmp[j]}
120
+ tmp = Roctave.dft(Roctave.postpad(a[i ... i+n], n))[0...nb_points]
121
+ ha = ha.collect.with_index{|v, j| v+tmp[j]}
122
+ end
123
+
124
+ h = (0...nb_points).collect{|i| hb[i] / ha[i]}
125
+
126
+ if do_plot and Roctave.respond_to?(:plot) then
127
+ fs /= 2.0 if region == :half
128
+ if plot_m then
129
+ title = '{/:Bold Magnitude response}'
130
+ if normalize_freqency then
131
+ xlabel = 'Normalized frequency (x {/Symbol p} rad/sample)'
132
+ if region == :half then
133
+ freq = f.collect{|v| v/fs}
134
+ xlim = [0, 1]
135
+ else
136
+ freq = f.collect{|v| 2.0*v/fs}
137
+ xlim = [0, 2]
138
+ end
139
+ else
140
+ xlabel = 'Frequency'
141
+ freq = f
142
+ xlim = [0, fs]
143
+ end
144
+ if log then
145
+ ylabel = 'Magnitude (dB)'
146
+ mag = h.collect{|v| 20*Math.log10(v.abs)}
147
+ else
148
+ ylabel = 'Magnitude'
149
+ mag = h.collect{|v| v.abs}
150
+ end
151
+ if region == :whole and shift then
152
+ xlim = xlim.collect{|v| v - xlim.last / 2.0}
153
+ offset = (freq[1] - freq[0])/2.0
154
+ freq = freq.collect{|v| v - freq.last / 2.0 - offset}
155
+ mag = Roctave.fftshift(mag)
156
+ end
157
+ Roctave.plot(freq, mag, title: title, xlabel: xlabel, ylabel: ylabel, xlim: xlim, grid: true)
158
+ end
159
+ if plot_p then
160
+ title = '{/:Bold Phase shift response}'
161
+ if normalize_freqency then
162
+ xlabel = 'Normalized frequency (x {/Symbol p} rad/sample)'
163
+ if region == :half then
164
+ freq = f.collect{|v| v/fs}
165
+ xlim = [0, 1]
166
+ else
167
+ freq = f.collect{|v| 2.0*v/fs}
168
+ xlim = [0, 2]
169
+ end
170
+ else
171
+ xlabel = 'Frequency'
172
+ freq = f
173
+ xlim = [0, fs]
174
+ end
175
+ pha = h.collect{|v| v.arg}
176
+ if region == :whole and shift then
177
+ xlim = xlim.collect{|v| v - xlim.last / 2.0}
178
+ offset = (freq[1] - freq[0])/2.0
179
+ freq = freq.collect{|v| v - freq.last / 2.0 - offset}
180
+ pha = Roctave.fftshift(pha)
181
+ end
182
+ pha = Roctave.unwrap(pha)
183
+ if degree then
184
+ ylabel = 'Phase (degree)'
185
+ pha = pha.collect{|v| v / Math::PI * 180.0}
186
+ else
187
+ ylabel = 'Phase (radians)'
188
+ end
189
+ Roctave.plot(freq, pha, title: title, xlabel: xlabel, ylabel: ylabel, xlim: xlim, grid: true)
190
+ end
191
+ if plot_d then
192
+ title = '{/:Bold Group delay}'
193
+ if normalize_freqency then
194
+ xlabel = 'Normalized frequency (x {/Symbol p} rad/sample)'
195
+ ylabel = 'Group delay (in sample time)'
196
+ if region == :half then
197
+ freq = f.collect{|v| v/fs}
198
+ xlim = [0, 1]
199
+ else
200
+ freq = f.collect{|v| 2.0*v/fs}
201
+ xlim = [0, 2]
202
+ end
203
+ else
204
+ xlabel = 'Frequency'
205
+ freq = f
206
+ xlim = [0, fs]
207
+ ylabel = 'Group delay (s)'
208
+ end
209
+ pha = h.collect{|v| v.arg}
210
+ if region == :whole and shift then
211
+ xlim = xlim.collect{|v| v - xlim.last / 2.0}
212
+ offset = (freq[1] - freq[0])/2.0
213
+ freq = freq.collect{|v| v - freq.last / 2.0 - offset}
214
+ pha = Roctave.fftshift(pha)
215
+ end
216
+ pha = Roctave.unwrap(pha)
217
+ dd = 2.0*(f.last - f.first) / (f.length - 1)
218
+ dd *= 2.0*Math::PI unless normalize_freqency
219
+ gd = Array.new(pha.length){0.0}
220
+ (1...pha.length-1).each do |i|
221
+ gd[i] = (pha[i-1] - pha[i+1]) / dd
222
+ end
223
+ gd[0] = gd[1]
224
+ gd[-1] = gd[-2]
225
+ Roctave.plot(freq, gd, title: title, xlabel: xlabel, ylabel: ylabel, xlim: xlim, grid: true)
226
+ end
227
+ end
228
+
229
+ [h, f]
230
+ end
231
+
232
+ end
233
+
@@ -0,0 +1,80 @@
1
+ ## Copyright (C) 2019 Théotime Bollengier <theotime.bollengier@gmail.com>
2
+ ##
3
+ ## This file is part of Roctave
4
+ ##
5
+ ## Roctave is free software: you can redistribute it and/or modify
6
+ ## it under the terms of the GNU General Public License as published by
7
+ ## the Free Software Foundation, either version 3 of the License, or
8
+ ## (at your option) any later version.
9
+ ##
10
+ ## Roctave is distributed in the hope that it will be useful,
11
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ ## GNU General Public License for more details.
14
+ ##
15
+ ## You should have received a copy of the GNU General Public License
16
+ ## along with Roctave. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+
19
+ module Roctave
20
+ class IirFilter
21
+ include Roctave::Filter
22
+
23
+ # @see Roctave.freqz
24
+ def freqz (*args)
25
+ Roctave.freqz(numerator, denominator, *args)
26
+ end
27
+
28
+ # Draw the impulse response.
29
+ # @param len [Integer] the length of the displayed response
30
+ # @see Roctave.stem
31
+ def impulse_response (len = 100, *args)
32
+ x = Array.new([1, len.to_i].max){0.0}
33
+ x[0] = 1.0
34
+ y = self.clone.filter x
35
+ Roctave.stem(y, *args)
36
+ end
37
+
38
+ # Draw the step response.
39
+ # @param len [Integer] the length of the displayed response
40
+ # @see Roctave.stem
41
+ def step_response (len = 100, *args)
42
+ x = Array.new([1, len.to_i].max){1.0}
43
+ y = self.clone.filter x
44
+ Roctave.stem(y, *args)
45
+ end
46
+
47
+ # @see Roctave.zplane
48
+ def zplane
49
+ Roctave.zplane(numerator, denominator)
50
+ end
51
+
52
+ # @return [Roctave::IirFilter]
53
+ def clone
54
+ Roctave::IirFilter.new numerator, denominator
55
+ end
56
+
57
+ # Returns a Butterworth IIR filter
58
+ # @see Roctave.butter
59
+ # @return [Roctave::IirFilter]
60
+ def self.butter (*args)
61
+ Roctave::IirFilter.new *Roctave.butter(*args)
62
+ end
63
+
64
+ # Returns a Chebyshev type I IIR filter
65
+ # @see Roctave.cheby1
66
+ # @return [Roctave::IirFilter]
67
+ def self.cheby1 (*args)
68
+ Roctave::IirFilter.new *Roctave.cheby1(*args)
69
+ end
70
+
71
+ # Returns a Chebyshev type II IIR filter
72
+ # @see Roctave.cheby2
73
+ # @return [Roctave::IirFilter]
74
+ def self.cheby2 (*args)
75
+ Roctave::IirFilter.new *Roctave.cheby2(*args)
76
+ end
77
+ end
78
+ end
79
+
80
+
@@ -0,0 +1,78 @@
1
+ ## Copyright (C) 2019 Théotime Bollengier <theotime.bollengier@gmail.com>
2
+ ##
3
+ ## This file is part of Roctave
4
+ ##
5
+ ## Roctave is free software: you can redistribute it and/or modify
6
+ ## it under the terms of the GNU General Public License as published by
7
+ ## the Free Software Foundation, either version 3 of the License, or
8
+ ## (at your option) any later version.
9
+ ##
10
+ ## Roctave is distributed in the hope that it will be useful,
11
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ ## GNU General Public License for more details.
14
+ ##
15
+ ## You should have received a copy of the GNU General Public License
16
+ ## along with Roctave. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+
19
+ module Roctave
20
+ ##
21
+ # Linearly interpolate between points.
22
+ # @param x [Array<Float>] X coordinates of given points
23
+ # @param y [Array] Y value of given points at X coordinates
24
+ # @param xi [Array<Float>] X coordinates of the requested interpolated points
25
+ # @return [Array] the Y value of interpolated points at xi coordinates
26
+ def self.interp1 (x, y, xi)
27
+ [x, y, xi].each.with_index do |a, i|
28
+ raise ArgumentError.new "Argument #{i+1} must be an array" unless a.kind_of?(Array)
29
+ end
30
+ raise ArgumentError.new "Argument 1 and 2 must be arrays of the same length" unless x.length == y.length
31
+
32
+ xr = x.reverse
33
+ xi.collect.with_index do |v, i|
34
+ leftmost = nil
35
+ leftmost_index = nil
36
+ x.each.with_index do |e, j|
37
+ if (e < v) or (e == v and leftmost != v) then
38
+ leftmost = e
39
+ leftmost_index = j
40
+ end
41
+ end
42
+ if leftmost.nil? then
43
+ leftmost = x.last
44
+ leftmost_index = x.length - 1
45
+ end
46
+
47
+ rightmost = nil
48
+ rightmost_index = nil
49
+ xr.each.with_index do |e, j|
50
+ if (e > v) or (e == v and rightmost != v) then
51
+ rightmost = e
52
+ rightmost_index = x.length - 1 - j
53
+ end
54
+ end
55
+ if rightmost.nil? then
56
+ rightmost = x.first
57
+ rightmost_index = 0
58
+ end
59
+
60
+ if leftmost_index > rightmost_index then
61
+ leftmost,rightmost = rightmost,leftmost
62
+ leftmost_index,rightmost_index = rightmost_index,leftmost_index
63
+ end
64
+
65
+ #puts "[#{i}] #{leftmost_index} - #{rightmost_index}"
66
+
67
+ ml = y[leftmost_index]
68
+ mr = y[rightmost_index]
69
+
70
+ if (rightmost - leftmost) == 0 then
71
+ (ml + mr) / 2.0
72
+ else
73
+ ml*(rightmost - v)/(rightmost - leftmost) + mr*(v - leftmost)/(rightmost - leftmost)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,748 @@
1
+ ## Copyright (C) 2019 Théotime Bollengier <theotime.bollengier@gmail.com>
2
+ ##
3
+ ## This file is part of Roctave
4
+ ##
5
+ ## Roctave is free software: you can redistribute it and/or modify
6
+ ## it under the terms of the GNU General Public License as published by
7
+ ## the Free Software Foundation, either version 3 of the License, or
8
+ ## (at your option) any later version.
9
+ ##
10
+ ## Roctave is distributed in the hope that it will be useful,
11
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ ## GNU General Public License for more details.
14
+ ##
15
+ ## You should have received a copy of the GNU General Public License
16
+ ## along with Roctave. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+
19
+ #begin
20
+ require 'gnuplot'
21
+
22
+ module Roctave
23
+
24
+ # @!group Plotting
25
+
26
+ ##
27
+ # Used internally to parse plot() arguments
28
+ def self.parse_plot_args (args)
29
+ datasets = []
30
+ cur_dataset = nil
31
+ state = :x_or_y
32
+
33
+ args.each.with_index do |arg, arg_index|
34
+ case state
35
+ when :x_or_y
36
+ unless cur_dataset.nil? then
37
+ if cur_dataset[:y].nil? then
38
+ cur_dataset[:y] = cur_dataset[:x]
39
+ cur_dataset[:x] = (0 ... cur_dataset[:y].length).to_a
40
+ end
41
+ datasets << cur_dataset
42
+ cur_dataset = nil
43
+ end
44
+ raise ArgumentError.new "Argument #{arg_index + 1} is expected to be an array, either X or Y" unless arg.kind_of?(Enumerable)
45
+ cur_dataset = {x: arg}
46
+ state = :y_or_fmt_or_property
47
+ when :y_or_fmt_or_property
48
+ if arg.kind_of?(Enumerable) then
49
+ cur_dataset[:y] = arg
50
+ raise ArgumentError.new "Arguments #{arg_index} and #{arg_index + 1}: arrays must have the same length" unless cur_dataset[:y].length == cur_dataset[:x].length
51
+ state = :fmt_or_property_or_x
52
+ elsif arg.kind_of?(String) then
53
+ cur_dataset[:y] = cur_dataset[:x]
54
+ cur_dataset[:x] = (0 ... cur_dataset[:y].length).to_a
55
+ state = :fmt_or_property_or_x
56
+ redo
57
+ elsif arg.kind_of?(Symbol) then
58
+ cur_dataset[:y] = cur_dataset[:x]
59
+ cur_dataset[:x] = (0 ... cur_dataset[:y].length).to_a
60
+ state = :property_or_x
61
+ redo
62
+ else
63
+ raise ArgumentError.new "Argument #{arg_index + 1} is expected to be the Y array or the FMT string"
64
+ end
65
+ when :fmt_or_property_or_x
66
+ if arg.kind_of?(Enumerable) then
67
+ state = :x_or_y
68
+ redo
69
+ elsif arg.kind_of?(Symbol) then
70
+ state = :property_or_x
71
+ redo
72
+ elsif arg.kind_of?(String) then
73
+ m = arg.match(/;([^;]+);/)
74
+ if m then
75
+ cur_dataset[:legend] = m[1]
76
+ arg.sub!(/;([^;]+);/, '')
77
+ end
78
+
79
+ if arg =~ /--/ then
80
+ cur_dataset[:linetype] = :dashed; arg.sub!(/--/, '')
81
+ elsif arg =~ /-\./ then
82
+ cur_dataset[:linetype] = :dashdotted; arg.sub!(/-\./, '')
83
+ elsif arg =~ /-/ then
84
+ cur_dataset[:linetype] = :solid; arg.sub!(/-/, '')
85
+ elsif arg =~ /:/ then
86
+ cur_dataset[:linetype] = :dotted; arg.sub!(/:/, '')
87
+ end
88
+
89
+ if arg =~ /\+/ then
90
+ cur_dataset[:pointtype] = :crosshair; arg.sub!(/\+/, '')
91
+ elsif arg =~ /o/ then
92
+ cur_dataset[:pointtype] = :circle; arg.sub!(/o/, '')
93
+ elsif arg =~ /\*/ then
94
+ cur_dataset[:pointtype] = :star; arg.sub!(/\*/, '')
95
+ elsif arg =~ /\./ then
96
+ cur_dataset[:pointtype] = :point; arg.sub!(/\./, '')
97
+ elsif arg =~ /x/ then
98
+ cur_dataset[:pointtype] = :cross; arg.sub!(/x/, '')
99
+ elsif arg =~ /s/ then
100
+ cur_dataset[:pointtype] = :square; arg.sub!(/s/, '')
101
+ elsif arg =~ /d/ then
102
+ cur_dataset[:pointtype] = :diamond; arg.sub!(/d/, '')
103
+ elsif arg =~ /\^/ then
104
+ cur_dataset[:pointtype] = :upwardFacingTriangle; arg.sub!(/\^/, '')
105
+ elsif arg =~ /v/ then
106
+ cur_dataset[:pointtype] = :downwardFacingTriangle; arg.sub!(/v/, '')
107
+ elsif arg =~ />/ then
108
+ cur_dataset[:pointtype] = :rightFacingTriangle; arg.sub!(/>/, '')
109
+ elsif arg =~ /</ then
110
+ cur_dataset[:pointtype] = :leftFacingTriangle; arg.sub!(/</, '')
111
+ elsif arg =~ /p/ then
112
+ cur_dataset[:pointtype] = :pentagram; arg.sub!(/p/, '')
113
+ elsif arg =~ /h/ then
114
+ cur_dataset[:pointtype] = :hexagram; arg.sub!(/h/, '')
115
+ end
116
+
117
+ if arg =~ /r/ then
118
+ cur_dataset[:color] = :red; arg.sub!(/r/, '')
119
+ elsif arg =~ /g/ then
120
+ cur_dataset[:color] = :green; arg.sub!(/g/, '')
121
+ elsif arg =~ /b/ then
122
+ cur_dataset[:color] = :blue; arg.sub!(/b/, '')
123
+ elsif arg =~ /c/ then
124
+ cur_dataset[:color] = :cyan; arg.sub!(/c/, '')
125
+ elsif arg =~ /m/ then
126
+ cur_dataset[:color] = :magenta; arg.sub!(/m/, '')
127
+ elsif arg =~ /y/ then
128
+ cur_dataset[:color] = :yellow; arg.sub!(/y/, '')
129
+ elsif arg =~ /k/ then
130
+ cur_dataset[:color] = :black; arg.sub!(/k/, '')
131
+ elsif arg =~ /w/ then
132
+ cur_dataset[:color] = :white; arg.sub!(/w/, '')
133
+ end
134
+
135
+
136
+ raise ArgumentError.new "Argument #{arg_index + 1}: cannot parse \"#{arg}\"" unless arg.empty?
137
+
138
+ state = :property_or_x
139
+ else
140
+ raise ArgumentError.new "Argument #{arg_index + 1} is expected to be the X array or the FMT string"
141
+ end
142
+ when :property_or_x
143
+ if arg.kind_of?(Enumerable) then
144
+ state = :x_or_y
145
+ redo
146
+ elsif arg.kind_of?(Symbol) then
147
+ case arg
148
+ when :linetype
149
+ state = :prop_linetype
150
+ when :linewidth
151
+ state = :prop_linewidth
152
+ when :dashtype
153
+ state = :prop_dashtype
154
+ when :color
155
+ state = :prop_color
156
+ when :pointtype
157
+ state = :prop_pointtype
158
+ when :pointsize
159
+ state = :prop_pointsize
160
+ #when :pointcolor
161
+ # state = :prop_pointcolor
162
+ when :legend
163
+ state = :prop_legend
164
+ when :filled
165
+ cur_dataset[:empty] = false
166
+ state = :property_or_x
167
+ when :empty
168
+ cur_dataset[:empty] = true
169
+ state = :property_or_x
170
+ else
171
+ raise RuntimeError.new "Unexpected FSM state (#{state}) while parsing argument #{arg_index+1}, sorry =("
172
+ end
173
+ else
174
+ raise ArgumentError.new "Argument #{arg_index + 1} is expected to be the X array or a property symbol"
175
+ end
176
+ when :prop_linetype
177
+ case arg
178
+ when '--'
179
+ cur_dataset[:linetype] = :dashed
180
+ when :dashed
181
+ cur_dataset[:linetype] = :dashed
182
+ when '-.'
183
+ cur_dataset[:linetype] = :dashdotted
184
+ when :dashdotted
185
+ cur_dataset[:linetype] = :dashdotted
186
+ when '-'
187
+ cur_dataset[:linetype] = :solid
188
+ when :solid
189
+ cur_dataset[:linetype] = :solid
190
+ when ':'
191
+ cur_dataset[:linetype] = :dotted
192
+ when :dotted
193
+ cur_dataset[:linetype] = :dotted
194
+ when :none
195
+ cur_dataset[:linetype] = false
196
+ else
197
+ raise ArgumentError.new "Argument #{arg_index + 1} (linetype) is expected to be either '--', '-.', '-', ':', :dashed, :dashdotted, :solid, :doted"
198
+ end
199
+ state = :property_or_x
200
+ when :prop_linewidth
201
+ raise ArgumentError.new "Argument #{arg_index + 1} (linewidth) is expected to be a numeric value" unless arg.kind_of?(Numeric)
202
+ cur_dataset[:linewidth] = arg.to_f
203
+ state = :property_or_x
204
+ when :prop_dashtype
205
+ raise ArgumentError.new "Argument #{arg_index + 1} (dashtype) is expected to be a string containing only [-._ ]" if not(arg.kind_of?(String)) or arg =~ /[^-\._ ]/
206
+ cur_dataset[:dashtype] = arg
207
+ state = :property_or_x
208
+ when :prop_color
209
+ case arg
210
+ when 'r'
211
+ cur_dataset[:color] = :red
212
+ when 'g'
213
+ cur_dataset[:color] = :green
214
+ when 'b'
215
+ cur_dataset[:color] = :blue
216
+ when 'c'
217
+ cur_dataset[:color] = :cyan
218
+ when 'm'
219
+ cur_dataset[:color] = :magenta
220
+ when 'y'
221
+ cur_dataset[:color] = :yellow
222
+ when 'k'
223
+ cur_dataset[:color] = :black
224
+ when 'w'
225
+ cur_dataset[:color] = :white
226
+ when String
227
+ cur_dataset[:color] = arg.to_sym
228
+ when Symbol
229
+ cur_dataset[:color] = arg
230
+ else
231
+ raise ArgumentError.new "Argument #{arg_index + 1} (color) unexpected \"#{arg}\""
232
+ end
233
+ state = :property_or_x
234
+ when :prop_pointtype
235
+ case arg
236
+ when '+'
237
+ cur_dataset[:pointtype] = :crosshair
238
+ when :crosshair
239
+ cur_dataset[:pointtype] = :crosshair
240
+ when 'o'
241
+ cur_dataset[:pointtype] = :circle
242
+ when :circle
243
+ cur_dataset[:pointtype] = :circle
244
+ when '*'
245
+ cur_dataset[:pointtype] = :star
246
+ when :star
247
+ cur_dataset[:pointtype] = :star
248
+ when '.'
249
+ cur_dataset[:pointtype] = :point
250
+ when :point
251
+ cur_dataset[:pointtype] = :point
252
+ when 'x'
253
+ cur_dataset[:pointtype] = :cross
254
+ when :cross
255
+ cur_dataset[:pointtype] = :cross
256
+ when 's'
257
+ cur_dataset[:pointtype] = :square
258
+ when :square
259
+ cur_dataset[:pointtype] = :square
260
+ when 'd'
261
+ cur_dataset[:pointtype] = :diamond
262
+ when :diamond
263
+ cur_dataset[:pointtype] = :diamond
264
+ when '^'
265
+ cur_dataset[:pointtype] = :upwardFacingTriangle
266
+ when :upwardFacingTriangle
267
+ cur_dataset[:pointtype] = :upwardFacingTriangle
268
+ when 'v'
269
+ cur_dataset[:pointtype] = :downwardFacingTriangle
270
+ when :downwardFacingTriangle
271
+ cur_dataset[:pointtype] = :downwardFacingTriangle
272
+ when '>'
273
+ cur_dataset[:pointtype] = :rightFacingTriangle
274
+ when :rightFacingTriangle
275
+ cur_dataset[:pointtype] = :rightFacingTriangle
276
+ when '<'
277
+ cur_dataset[:pointtype] = :leftFacingTriangle
278
+ when :leftFacingTriangle
279
+ cur_dataset[:pointtype] = :leftFacingTriangle
280
+ when 'p'
281
+ cur_dataset[:pointtype] = :pentagram
282
+ when :pentagram
283
+ cur_dataset[:pointtype] = :pentagram
284
+ when 'h'
285
+ cur_dataset[:pointtype] = :hexagram
286
+ when :hexagram
287
+ cur_dataset[:pointtype] = :hexagram
288
+ when :none
289
+ cur_dataset[:pointtype] = false
290
+ else
291
+ raise ArgumentError.new "Argument #{arg_index + 1} (pointtype) unexpected \"#{arg}\""
292
+ end
293
+ state = :property_or_x
294
+ when :prop_pointsize
295
+ raise ArgumentError.new "Argument #{arg_index + 1} (pointsize) is expected to be a numeric value" unless arg.kind_of?(Numeric)
296
+ cur_dataset[:pointsize] = arg.to_f
297
+ state = :property_or_x
298
+ #when :prop_pointcolor
299
+ # case arg
300
+ # when 'k'
301
+ # cur_dataset[:pointcolor] = :black
302
+ # when 'r'
303
+ # cur_dataset[:pointcolor] = :red
304
+ # when 'g'
305
+ # cur_dataset[:pointcolor] = :green
306
+ # when 'b'
307
+ # cur_dataset[:pointcolor] = :blue
308
+ # when 'm'
309
+ # cur_dataset[:pointcolor] = :magenta
310
+ # when 'c'
311
+ # cur_dataset[:pointcolor] = :cyan
312
+ # when 'w'
313
+ # cur_dataset[:pointcolor] = :white
314
+ # when String
315
+ # cur_dataset[:pointcolor] = arg.to_sym
316
+ # when Symbol
317
+ # cur_dataset[:pointcolor] = arg
318
+ # else
319
+ # raise ArgumentError.new "Argument #{arg_index + 1} (pointcolor) unexpected \"#{arg}\""
320
+ # end
321
+ # state = :property_or_x
322
+ when :prop_legend
323
+ raise ArgumentError.new "Argument #{arg_index + 1} (legend) is expected to be a string" unless arg.kind_of?(String)
324
+ cur_dataset[:legend] = arg
325
+ state = :property_or_x
326
+ else
327
+ raise RuntimeError.new "Unexpected FSM state (#{state}) while parsing argument #{arg_index+1}, sorry =("
328
+ end
329
+ end
330
+ unless cur_dataset.nil? then
331
+ if cur_dataset[:y].nil? then
332
+ cur_dataset[:y] = cur_dataset[:x]
333
+ cur_dataset[:x] = (0 ... cur_dataset[:y].length).to_a
334
+ end
335
+ datasets << cur_dataset
336
+ end
337
+ datasets
338
+ end
339
+
340
+
341
+ # @param args [Array, String] plot(Y), plot(X, Y), plot(X, Y, FMT), plot(X1, Y1, ..., Xn, Yn). FMT: Linestyle: '-' solid, '--' dashed', ':' dotted, '-.' dash-dotted. Marker: '+' crosshair, 'o' circle, '*' star, '*' star, '.' point, 'x' cross, 's' square, 'd' diamond, '^' upward-facing triangle, 'v' downward-facing triangle, '>' right-facing triangle, '<' left-facing triangle, 'p' pentagram, 'h' hexagram. Color: 'k' blacK, 'r' Red, 'g' Green, 'b' Blue, 'm' Magenta, 'c' Cyan, 'w' White, ";displayname;"
342
+ # @param title [String] Graph title
343
+ # @param xlabel [String] X axix label
344
+ # @param ylabel [String] X axix label
345
+ # @param xlim [Array<Float, Float>, Range] Two element array of the leftmost and rightmost X axis limits
346
+ # @param ylim [Array<Float, Float>, Range] Two element array of the lower and upper Y axis limits
347
+ # @param logx [Integer] Enable log scaling of the X axis. The given number is the base. 0 if for 10
348
+ # @param logy [Integer] Enable log scaling of the Y axis. The given number is the base. 0 if for 10
349
+ # @param grid [Boolean] Display the grid
350
+ # @param terminal [String] The command you would issue to gnuplot to set the terminal, without the 'set terminal ', ex: 'png transparent enhanced'
351
+ # @param output [String] The path of an output file if the terminal allows it.
352
+ def self.plot (*args, title: nil, xlabel: nil, ylabel: nil, xlim: nil, ylim: nil, logx: nil, logy: nil, grid: true, terminal: 'wxt enhanced persist', output: nil, **opts)
353
+ datasets = Roctave.parse_plot_args(args)
354
+ return nil if datasets.empty?
355
+
356
+ Gnuplot.open do |gp|
357
+ Gnuplot::Plot.new(gp) do |plot|
358
+ if terminal then
359
+ plot.terminal terminal.to_s
360
+ plot.output output.to_s if output
361
+ end
362
+ plot.title title if title.kind_of?(String)
363
+ plot.xlabel xlabel if xlabel.kind_of?(String)
364
+ plot.ylabel ylabel if ylabel.kind_of?(String)
365
+ plot.xrange "[#{xlim.first}:#{xlim.last}]" if xlim.kind_of?(Array) and xlim.length == 2 and xlim.first.kind_of?(Numeric) and xlim.last.kind_of?(Numeric)
366
+ plot.xrange "[#{xlim.begin}:#{xlim.end}]" if xlim.kind_of?(Range) and xlim.begin.kind_of?(Numeric) and xlim.end.kind_of?(Numeric)
367
+ plot.yrange "[#{ylim.first}:#{ylim.last}]" if ylim.kind_of?(Array) and ylim.length == 2 and ylim.first.kind_of?(Numeric) and ylim.last.kind_of?(Numeric)
368
+ plot.yrange "[#{ylim.begin}:#{ylim.end}]" if ylim.kind_of?(Range) and ylim.begin.kind_of?(Numeric) and ylim.end.kind_of?(Numeric)
369
+ plot.logscale "x#{(logx.to_i > 0) ? " #{logx.to_i}" : ''}" if logx
370
+ plot.logscale "y#{(logy.to_i > 0) ? " #{logy.to_i}" : ''}" if logy
371
+
372
+ ## Matlab colors ##
373
+ plot.linetype "1 lc rgb '#0072BD'"
374
+ plot.linetype "2 lc rgb '#D95319'"
375
+ plot.linetype "3 lc rgb '#EDB120'"
376
+ plot.linetype "4 lc rgb '#7E2F8E'"
377
+ plot.linetype "5 lc rgb '#77AC30'"
378
+ plot.linetype "6 lc rgb '#4DBEEE'"
379
+ plot.linetype "7 lc rgb '#A2142F'"
380
+ plot.linetype "192 dt solid lc rgb '#df000000'"
381
+
382
+ if grid == true then
383
+ plot.grid 'ls 192'
384
+ elsif grid then
385
+ plot.grid grid.to_s
386
+ end
387
+
388
+ opts.each do |k, v|
389
+ plot.send(k.to_sym, v.to_s)
390
+ end
391
+
392
+ plot.data = datasets.collect do |ds|
393
+ Gnuplot::DataSet.new([ds[:x], ds[:y]]) { |gpds|
394
+ if ds[:legend].kind_of?(String) then
395
+ gpds.title = ds[:legend]
396
+ else
397
+ gpds.notitle
398
+ end
399
+ gpds.linecolor = "'#{ds[:color]}'" if ds[:color]
400
+ gpds.linewidth = "#{ds[:linewidth]}" if ds[:linewidth]
401
+
402
+ with = ''
403
+ if (ds[:linetype] or ds[:dashtype] or (ds[:pointtype].nil? and ds[:pointsize].nil?)) then
404
+ with += 'lines'
405
+ linetype = ''
406
+ else
407
+ linetype = nil
408
+ end
409
+ if ds[:pointtype] or ds[:pointsize] then
410
+ with += 'points'
411
+ pointtype = ''
412
+ else
413
+ pointtype = nil
414
+ end
415
+
416
+ if ds[:linetype] == :dashed then
417
+ linetype += ' dt "-"'
418
+ elsif ds[:linetype] == :dotted then
419
+ linetype += ' dt "."'
420
+ elsif ds[:linetype] == :dashdotted then
421
+ linetype += ' dt "_. "'
422
+ end
423
+
424
+ case ds[:pointtype]
425
+ when :crosshair
426
+ pointtype += ' pt 1'
427
+ when :circle
428
+ if ds[:empty] == false then
429
+ pointtype += ' pt 7' # filled
430
+ else
431
+ pointtype += ' pt 6' # emtpy
432
+ end
433
+ when :star
434
+ pointtype += ' pt 3'
435
+ when :point
436
+ if ds[:empty] then
437
+ pointtype += ' pt 6' # emtpy
438
+ else
439
+ pointtype += ' pt 7' # filled
440
+ end
441
+ when :cross
442
+ pointtype += ' pt 2'
443
+ when :square
444
+ if ds[:empty] then
445
+ pointtype += ' pt 4' # emtpy
446
+ else
447
+ pointtype += ' pt 5' # filled
448
+ end
449
+ when :diamond
450
+ if ds[:empty] then
451
+ pointtype += ' pt 12' # emtpy
452
+ else
453
+ pointtype += ' pt 13' # filled
454
+ end
455
+ when :upwardFacingTriangle
456
+ if ds[:empty] then
457
+ pointtype += ' pt 8' # emtpy
458
+ else
459
+ pointtype += ' pt 9' # filled
460
+ end
461
+ when :downwardFacingTriangle
462
+ if ds[:empty] then
463
+ pointtype += ' pt 10' # emtpy
464
+ else
465
+ pointtype += ' pt 11' # filled
466
+ end
467
+ when :rightFacingTriangle
468
+ if ds[:empty] == false then
469
+ pointtype += ' pt 9' # filled
470
+ else
471
+ pointtype += ' pt 8' # emtpy
472
+ end
473
+ when :leftFacingTriangle
474
+ if ds[:empty] == false then
475
+ pointtype += ' pt 11' # filled
476
+ else
477
+ pointtype += ' pt 10' # emtpy
478
+ end
479
+ when :pentagram
480
+ if ds[:empty] then
481
+ pointtype += ' pt 14' # emtpy
482
+ else
483
+ pointtype += ' pt 15' # filled
484
+ end
485
+ when :hexagram
486
+ if ds[:empty] == false then
487
+ pointtype += ' pt 15' # filled
488
+ else
489
+ pointtype += ' pt 14' # emtpy
490
+ end
491
+ end
492
+
493
+ unless (linetype.nil? or ds[:dashtype].nil?) then
494
+ linetype.gsub!(/ dt "[-\._ ]"/, '') if linetype =~ / dt "[-\._ ]"/
495
+ linetype += " dt \"#{ds[:dashtype]}\""
496
+ end
497
+ pointtype += " ps #{ds[:pointsize]}" if (pointtype and ds[:pointsize])
498
+
499
+ with += linetype if linetype
500
+ with += pointtype if pointtype
501
+ gpds.with = with
502
+ }
503
+ end
504
+ end
505
+ end
506
+ end
507
+
508
+
509
+ # @param args [Array, String] plot(Y), plot(X, Y), plot(X, Y, FMT), plot(X1, Y1, ..., Xn, Yn). FMT: Linestyle: '-' solid, '--' dashed', ':' dotted, '-.' dash-dotted. Marker: '+' crosshair, 'o' circle, '*' star, '*' star, '.' point, 'x' cross, 's' square, 'd' diamond, '^' upward-facing triangle, 'v' downward-facing triangle, '>' right-facing triangle, '<' left-facing triangle, 'p' pentagram, 'h' hexagram. Color: 'k' blacK, 'r' Red, 'g' Green, 'b' Blue, 'm' Magenta, 'c' Cyan, 'w' White, ";displayname;"
510
+ # @param title [String] Graph title
511
+ # @param xlabel [String] X axix label
512
+ # @param ylabel [String] X axix label
513
+ # @param xlim [Array<Float, Float>, Range] Two element array of the leftmost and rightmost X axis limits
514
+ # @param ylim [Array<Float, Float>, Range] Two element array of the lower and upper Y axis limits
515
+ # @param logx [Integer] Enable log scaling of the X axis. The given number is the base. 0 if for 10
516
+ # @param logy [Integer] Enable log scaling of the Y axis. The given number is the base. 0 if for 10
517
+ # @param grid [Boolean] Display the grid
518
+ # @param terminal [String] The command you would issue to gnuplot to set the terminal, without the 'set terminal ', ex: 'png transparent enhanced'
519
+ # @param output [String] The path of an output file if the terminal allows it.
520
+ def self.stem (*args, title: nil, xlabel: nil, ylabel: nil, xlim: nil, ylim: nil, logx: nil, logy: nil, grid: true, terminal: 'wxt enhanced persist', output: nil, **opts)
521
+ datasets = Roctave.parse_plot_args(args)
522
+ return nil if datasets.empty?
523
+
524
+ Gnuplot.open do |gp|
525
+ Gnuplot::Plot.new(gp) do |plot|
526
+ if terminal then
527
+ plot.terminal terminal.to_s
528
+ plot.output output.to_s if output
529
+ end
530
+ plot.title title if title.kind_of?(String)
531
+ plot.xlabel xlabel if xlabel.kind_of?(String)
532
+ plot.ylabel ylabel if ylabel.kind_of?(String)
533
+ plot.xrange "[#{xlim.first}:#{xlim.last}]" if xlim.kind_of?(Array) and xlim.length == 2 and xlim.first.kind_of?(Numeric) and xlim.last.kind_of?(Numeric)
534
+ plot.xrange "[#{xlim.begin}:#{xlim.end}]" if xlim.kind_of?(Range) and xlim.begin.kind_of?(Numeric) and xlim.end.kind_of?(Numeric)
535
+ plot.yrange "[#{ylim.first}:#{ylim.last}]" if ylim.kind_of?(Array) and ylim.length == 2 and ylim.first.kind_of?(Numeric) and ylim.last.kind_of?(Numeric)
536
+ plot.yrange "[#{ylim.begin}:#{ylim.end}]" if ylim.kind_of?(Range) and ylim.begin.kind_of?(Numeric) and ylim.end.kind_of?(Numeric)
537
+ plot.logscale "x#{(logx.to_i > 0) ? " #{logx.to_i}" : ''}" if logx
538
+ plot.logscale "y#{(logy.to_i > 0) ? " #{logy.to_i}" : ''}" if logy
539
+
540
+ ## Matlab colors ##
541
+ plot.linetype "1 lc rgb '#0072BD'"
542
+ plot.linetype "2 lc rgb '#D95319'"
543
+ plot.linetype "3 lc rgb '#EDB120'"
544
+ plot.linetype "4 lc rgb '#7E2F8E'"
545
+ plot.linetype "5 lc rgb '#77AC30'"
546
+ plot.linetype "6 lc rgb '#4DBEEE'"
547
+ plot.linetype "7 lc rgb '#A2142F'"
548
+ plot.linetype "192 dt solid lc rgb '#df000000'"
549
+
550
+ if grid == true then
551
+ plot.grid 'ls 192'
552
+ elsif grid then
553
+ plot.grid grid.to_s
554
+ end
555
+
556
+ opts.each do |k, v|
557
+ plot.send(k.to_sym, v.to_s)
558
+ end
559
+
560
+ linetype_counter = 1
561
+
562
+ datasets.each do |ds|
563
+ ds[:linetype] = :solid if ds[:linetype].nil?
564
+ ds[:pointtype] = :circle if ds[:pointtype].nil? or (ds[:pointtype] == false and ds[:linetype] == false)
565
+ unless ds[:linetype] == false then
566
+ plot.data << Gnuplot::DataSet.new([ds[:x], ds[:y]]) { |gpds|
567
+ if ds[:legend].kind_of?(String) and ds[:pointtype] == false then
568
+ gpds.title = ds[:legend]
569
+ else
570
+ gpds.notitle
571
+ end
572
+ gpds.notitle
573
+ gpds.linecolor = "'#{ds[:color]}'" if ds[:color]
574
+ gpds.linewidth = "#{ds[:linewidth]}" if ds[:linewidth]
575
+ with = 'impulses'
576
+ linetype = nil
577
+ if (ds[:linetype] or ds[:dashtype] or (ds[:pointtype].nil? and ds[:pointsize].nil?)) then
578
+ linetype = ''
579
+ end
580
+
581
+ with = 'impulses'
582
+ if ds[:linetype] == :dashed then
583
+ linetype += ' dt "-"'
584
+ elsif ds[:linetype] == :dotted then
585
+ linetype += ' dt "."'
586
+ elsif ds[:linetype] == :dashdotted then
587
+ linetype += ' dt "_. "'
588
+ end
589
+
590
+ unless (linetype.nil? or ds[:dashtype].nil?) then
591
+ linetype.gsub!(/ dt "[-\._ ]"/, '') if linetype =~ / dt "[-\._ ]"/
592
+ linetype += " dt \"#{ds[:dashtype]}\""
593
+ end
594
+ with += " lt #{linetype_counter}"
595
+ with += linetype if linetype
596
+ gpds.with = with
597
+ }
598
+ end
599
+ unless ds[:pointtype] == false then
600
+ plot.data << Gnuplot::DataSet.new([ds[:x], ds[:y]]) { |gpds|
601
+ if ds[:legend].kind_of?(String) then
602
+ gpds.title = ds[:legend]
603
+ else
604
+ gpds.notitle
605
+ end
606
+
607
+ gpds.linecolor = "'#{ds[:color]}'" if ds[:color]
608
+ gpds.linewidth = "#{ds[:linewidth]}" if ds[:linewidth]
609
+
610
+ with = 'points'
611
+ if ds[:pointtype] or ds[:pointsize] then
612
+ pointtype = ''
613
+ else
614
+ pointtype = nil
615
+ end
616
+
617
+ case ds[:pointtype]
618
+ when :crosshair
619
+ pointtype += ' pt 1'
620
+ when :circle
621
+ if ds[:empty] == false then
622
+ pointtype += ' pt 7' # filled
623
+ else
624
+ pointtype += ' pt 6' # emtpy
625
+ end
626
+ when :star
627
+ pointtype += ' pt 3'
628
+ when :point
629
+ if ds[:empty] then
630
+ pointtype += ' pt 6' # emtpy
631
+ else
632
+ pointtype += ' pt 7' # filled
633
+ end
634
+ when :cross
635
+ pointtype += ' pt 2'
636
+ when :square
637
+ if ds[:empty] then
638
+ pointtype += ' pt 4' # emtpy
639
+ else
640
+ pointtype += ' pt 5' # filled
641
+ end
642
+ when :diamond
643
+ if ds[:empty] then
644
+ pointtype += ' pt 12' # emtpy
645
+ else
646
+ pointtype += ' pt 13' # filled
647
+ end
648
+ when :upwardFacingTriangle
649
+ if ds[:empty] then
650
+ pointtype += ' pt 8' # emtpy
651
+ else
652
+ pointtype += ' pt 9' # filled
653
+ end
654
+ when :downwardFacingTriangle
655
+ if ds[:empty] then
656
+ pointtype += ' pt 10' # emtpy
657
+ else
658
+ pointtype += ' pt 11' # filled
659
+ end
660
+ when :rightFacingTriangle
661
+ if ds[:empty] == false then
662
+ pointtype += ' pt 9' # filled
663
+ else
664
+ pointtype += ' pt 8' # emtpy
665
+ end
666
+ when :leftFacingTriangle
667
+ if ds[:empty] == false then
668
+ pointtype += ' pt 11' # filled
669
+ else
670
+ pointtype += ' pt 10' # emtpy
671
+ end
672
+ when :pentagram
673
+ if ds[:empty] then
674
+ pointtype += ' pt 14' # emtpy
675
+ else
676
+ pointtype += ' pt 15' # filled
677
+ end
678
+ when :hexagram
679
+ if ds[:empty] == false then
680
+ pointtype += ' pt 15' # filled
681
+ else
682
+ pointtype += ' pt 14' # emtpy
683
+ end
684
+ end
685
+
686
+ pointtype += " ps #{ds[:pointsize]}" if (pointtype and ds[:pointsize])
687
+
688
+ with += " lt #{linetype_counter}"
689
+ with += pointtype if pointtype
690
+
691
+ gpds.with = with
692
+ }
693
+ end
694
+ linetype_counter += 1
695
+ end
696
+ end
697
+ end
698
+ end
699
+
700
+
701
+ ##
702
+ # Plot the poles and zeros. The arguments
703
+ # represent filter coefficients (numerator polynomial b and
704
+ # denominator polynomial a).
705
+ #
706
+ # Note that due to the nature of the roots() function, poles and
707
+ # zeros may be displayed as occurring around a circle rather than at
708
+ # a single point.
709
+ # @param b [Array<Numeric>] Filter numerator polynomial coefficients
710
+ # @param a [Array<Numeric>] Filter denominator polynomial coefficients
711
+ def self.zplane (b, a = [])
712
+ m = b.length - 1
713
+ n = a.length - 1
714
+ b = [1.0] if b.empty?
715
+ a = [1.0] if b.empty?
716
+ z = Roctave.roots(b) + Roctave.zeros(n - m)
717
+ p = Roctave.roots(a) + Roctave.zeros(m - n)
718
+
719
+ z_real = z.collect{|v| v.real}
720
+ p_real = p.collect{|v| v.real}
721
+ z_imag = z.collect{|v| v.imag}
722
+ p_imag = p.collect{|v| v.imag}
723
+ reala = z_real + p_real
724
+ imaga = z_imag + p_imag
725
+
726
+ xmin = [-1.0, reala.min].min.to_f
727
+ xmax = [ 1.0, reala.max].max.to_f
728
+ ymin = [-1.0, imaga.min].min.to_f
729
+ ymax = [ 1.0, imaga.max].max.to_f
730
+ xfluff = [0.05*(xmax-xmin), (1.05*(ymax-ymin)-(xmax-xmin))/10.0].max
731
+ yfluff = [0.05*(ymax-ymin), (1.05*(xmax-xmin)-(ymax-ymin))/10.0].max
732
+ xmin = xmin - xfluff
733
+ xmax = xmax + xfluff
734
+ ymin = ymin - yfluff
735
+ ymax = ymax + yfluff
736
+
737
+ rx = (0..100).collect{|i| Math.cos(2*Math::PI*i/100)}
738
+ ry = (0..100).collect{|i| Math.sin(2*Math::PI*i/100)}
739
+
740
+ Roctave.plot(z_real, z_imag, 'o;Zeros;', :pointsize, 1.5, p_real, p_imag, 'x;Poles;', :pointsize, 1.5, rx, ry, '-', :color, '#505050', xlabel: 'Real', ylabel: 'Imaginary', title: '{/:Bold Z-plane}', grid: true, xlim: [xmin, xmax], ylim: [ymin, ymax], size: 'ratio -1')
741
+ nil
742
+ end
743
+
744
+ end
745
+ #rescue LoadError => e
746
+ # STDERR.puts "WARNING: #{e.message}\nPloting functions will not be available."
747
+ #end
748
+