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