roctave 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +33 -0
- data/ext/roctave/cu8_file_reader.c +331 -0
- data/ext/roctave/cu8_file_reader.h +30 -0
- data/ext/roctave/extconf.rb +6 -0
- data/ext/roctave/fir_filter.c +795 -0
- data/ext/roctave/fir_filter.h +29 -0
- data/ext/roctave/freq_shifter.c +410 -0
- data/ext/roctave/freq_shifter.h +29 -0
- data/ext/roctave/iir_filter.c +462 -0
- data/ext/roctave/iir_filter.h +29 -0
- data/ext/roctave/roctave.c +38 -0
- data/ext/roctave/roctave.h +27 -0
- data/lib/roctave.rb +168 -0
- data/lib/roctave/bilinear.rb +92 -0
- data/lib/roctave/butter.rb +87 -0
- data/lib/roctave/cheby.rb +180 -0
- data/lib/roctave/cu8_file_reader.rb +45 -0
- data/lib/roctave/dft.rb +280 -0
- data/lib/roctave/filter.rb +64 -0
- data/lib/roctave/finite_difference_coefficients.rb +73 -0
- data/lib/roctave/fir.rb +121 -0
- data/lib/roctave/fir1.rb +134 -0
- data/lib/roctave/fir2.rb +246 -0
- data/lib/roctave/fir_design.rb +311 -0
- data/lib/roctave/firls.rb +380 -0
- data/lib/roctave/firpm.rb +499 -0
- data/lib/roctave/freq_shifter.rb +47 -0
- data/lib/roctave/freqz.rb +233 -0
- data/lib/roctave/iir.rb +80 -0
- data/lib/roctave/interp1.rb +78 -0
- data/lib/roctave/plot.rb +748 -0
- data/lib/roctave/poly.rb +46 -0
- data/lib/roctave/roots.rb +73 -0
- data/lib/roctave/sftrans.rb +157 -0
- data/lib/roctave/version.rb +3 -0
- data/lib/roctave/window.rb +116 -0
- data/roctave.gemspec +79 -0
- data/samples/butter.rb +12 -0
- data/samples/cheby.rb +28 -0
- data/samples/dft.rb +18 -0
- data/samples/differentiator.rb +48 -0
- data/samples/differentiator_frequency_scaling.rb +52 -0
- data/samples/fft.rb +40 -0
- data/samples/finite_difference_coefficient.rb +53 -0
- data/samples/fir1.rb +13 -0
- data/samples/fir2.rb +14 -0
- data/samples/fir2_windows.rb +29 -0
- data/samples/fir2bank.rb +30 -0
- data/samples/fir_low_pass.rb +44 -0
- data/samples/firls.rb +77 -0
- data/samples/firpm.rb +78 -0
- data/samples/hilbert_transformer.rb +20 -0
- data/samples/hilbert_transformer_frequency_scaling.rb +47 -0
- data/samples/plot.rb +45 -0
- data/samples/stem.rb +8 -0
- data/samples/type1.rb +25 -0
- data/samples/type3.rb +24 -0
- data/samples/windows.rb +25 -0
- 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
|
+
|
data/lib/roctave/iir.rb
ADDED
@@ -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
|
+
|
data/lib/roctave/plot.rb
ADDED
@@ -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
|
+
|