radio 0.0.2 → 0.0.3
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.
- data/README.md +7 -6
- data/lib/radio.rb +5 -4
- data/lib/radio/controls/civ.rb +284 -0
- data/lib/radio/controls/si570avr.rb +0 -1
- data/lib/radio/filter.rb +33 -34
- data/lib/radio/filters/agc.rb +65 -0
- data/lib/radio/filters/fir.rb +264 -33
- data/lib/radio/filters/iq.rb +142 -0
- data/lib/radio/gif.rb +38 -40
- data/lib/radio/psk31/rx.rb +17 -7
- data/lib/radio/rig.rb +25 -3
- data/lib/radio/rig/rx.rb +11 -6
- data/lib/radio/rig/spectrum.rb +54 -43
- data/lib/radio/rig/ssb.rb +190 -0
- data/lib/radio/rig/ssb.rb_ +150 -0
- data/lib/radio/{input.rb → signal.rb} +16 -12
- data/lib/radio/{inputs → signals}/alsa.rb +20 -23
- data/lib/radio/signals/coreaudio.rb +136 -0
- data/lib/radio/{inputs → signals}/file.rb +8 -8
- data/lib/radio/{inputs → signals}/wav.rb +41 -28
- data/lib/radio/utils/firpm.rb +395 -0
- data/lib/radio/utils/misc.rb +39 -0
- data/lib/radio/utils/window.rb +37 -0
- data/lib/radio/version.rb +1 -1
- data/www/index.erb +81 -12
- data/www/lo.erb +2 -2
- data/www/setup/af.erb +72 -0
- data/www/setup/lo.erb +31 -0
- data/www/setup/{input.erb → rx.erb} +11 -10
- data/www/ssb.erb +20 -0
- data/www/tune.erb +5 -0
- data/www/waterfall.erb +1 -1
- metadata +38 -26
- data/lib/radio/inputs/coreaudio.rb +0 -102
- data/lib/radio/psk31/fir_coef.rb +0 -292
- data/test/test.rb +0 -76
- data/test/wav/bpsk8k.wav +0 -0
- data/test/wav/qpsk8k.wav +0 -0
- data/test/wav/ssb.wav +0 -0
@@ -15,31 +15,33 @@
|
|
15
15
|
|
16
16
|
|
17
17
|
class Radio
|
18
|
-
module
|
18
|
+
module Signal
|
19
19
|
class File
|
20
20
|
class WAV
|
21
21
|
|
22
22
|
attr_reader :rate
|
23
23
|
|
24
|
-
def initialize
|
25
|
-
@file = ::File.new id
|
24
|
+
def initialize options
|
25
|
+
@file = ::File.new options[:id]
|
26
26
|
#TODO validate header instead?
|
27
27
|
@file.read 12 # discard header
|
28
|
-
@rate = rate
|
29
|
-
|
30
|
-
|
28
|
+
@rate = options[:rate].to_i
|
29
|
+
if input = options[:input]
|
30
|
+
@channel_i = input[0]
|
31
|
+
@channel_q = input[1]
|
32
|
+
end
|
31
33
|
@data = [next_data]
|
32
34
|
@time = Time.now
|
33
35
|
end
|
34
36
|
|
35
|
-
def
|
37
|
+
def in samples
|
36
38
|
sample_size = @channels * (@bit_sample/8)
|
37
|
-
@time += 1.0/(rate/samples)
|
39
|
+
@time += 1.0/(rate.to_f/samples)
|
38
40
|
sleep [0,@time-Time.now].max
|
39
41
|
while @data.reduce(0){|a,b|a+b.size} < samples * sample_size
|
40
42
|
@data.push next_data
|
41
43
|
end
|
42
|
-
if
|
44
|
+
if input_channels > 1
|
43
45
|
out = NArray.scomplex samples
|
44
46
|
else
|
45
47
|
out = NArray.sfloat samples
|
@@ -59,11 +61,15 @@ class Radio
|
|
59
61
|
out
|
60
62
|
end
|
61
63
|
|
62
|
-
def
|
64
|
+
def input_channels
|
63
65
|
return 2 if @channel_q and @channels > 1
|
64
66
|
1
|
65
67
|
end
|
66
68
|
|
69
|
+
def output_channels
|
70
|
+
0
|
71
|
+
end
|
72
|
+
|
67
73
|
def stop
|
68
74
|
@file.close
|
69
75
|
end
|
@@ -78,9 +84,10 @@ class Radio
|
|
78
84
|
else
|
79
85
|
raise "Unsupported sample size: #{@bit_sample}"
|
80
86
|
end
|
81
|
-
return out if
|
87
|
+
return out if input_channels == 1 and @channels == 1
|
88
|
+
#TODO buggy wav files with bad endings raise the next line
|
82
89
|
out.reshape! @channels, out.size/@channels
|
83
|
-
if
|
90
|
+
if input_channels == 1
|
84
91
|
out[@channel_i,true]
|
85
92
|
else
|
86
93
|
c_out = NArray.scomplex out[0,true].size
|
@@ -90,26 +97,32 @@ class Radio
|
|
90
97
|
end
|
91
98
|
end
|
92
99
|
|
93
|
-
#TODO read data in chunks smaller than size (which is often the whole file)
|
94
100
|
def next_data
|
95
101
|
loop do
|
96
102
|
until @file.eof?
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
@id = fmt.slice(0,2).unpack('c')[0]
|
103
|
-
@channels = fmt.slice(2,2).unpack('c')[0]
|
104
|
-
@rate = fmt.slice(4,4).unpack('V').join.to_i
|
105
|
-
@byte_sec = fmt.slice(8,4).unpack('V').join.to_i
|
106
|
-
@block_size = fmt.slice(12,2).unpack('c')[0]
|
107
|
-
@bit_sample = fmt.slice(14,2).unpack('c')[0]
|
108
|
-
next
|
109
|
-
when 'data'
|
110
|
-
return @file.read size
|
103
|
+
if @data_size
|
104
|
+
actual = [@data_size, 4096].min
|
105
|
+
return @file.read actual
|
106
|
+
@data_size -= actual
|
107
|
+
@data_size = nil if @data_size == 0
|
111
108
|
else
|
112
|
-
|
109
|
+
type = @file.read(4)
|
110
|
+
size = @file.read(4).unpack("V")[0].to_i
|
111
|
+
case type
|
112
|
+
when 'fmt '
|
113
|
+
fmt = @file.read(size)
|
114
|
+
@id = fmt.slice(0,2).unpack('c')[0]
|
115
|
+
@channels = fmt.slice(2,2).unpack('c')[0]
|
116
|
+
@rate = fmt.slice(4,4).unpack('V').join.to_i
|
117
|
+
@byte_sec = fmt.slice(8,4).unpack('V').join.to_i
|
118
|
+
@block_size = fmt.slice(12,2).unpack('c')[0]
|
119
|
+
@bit_sample = fmt.slice(14,2).unpack('c')[0]
|
120
|
+
next
|
121
|
+
when 'data'
|
122
|
+
@data_size = size
|
123
|
+
else
|
124
|
+
raise "Unknown type: #{type}"
|
125
|
+
end
|
113
126
|
end
|
114
127
|
end
|
115
128
|
@file.rewind
|
@@ -0,0 +1,395 @@
|
|
1
|
+
# Copyright 2012 The ham21/radio Authors
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
# This uses similar variable names and similar logic as a
|
17
|
+
# popular C implementation of the Parks McClellan algorithm
|
18
|
+
# which is based on an old FORTRAN implementation.
|
19
|
+
# I did this Ruby version in a hurry so for comments, see the C:
|
20
|
+
# http://www.janovetz.com/ Jake Janovetz (janovetz@uiuc.edu)
|
21
|
+
|
22
|
+
|
23
|
+
class Radio
|
24
|
+
module Utils
|
25
|
+
|
26
|
+
FIRPM_CACHE_VERSION = 1
|
27
|
+
FIRPM_CACHE_FILENAME = File.expand_path '~/.radio_firpm_cache'
|
28
|
+
@@firpm_cache = {}
|
29
|
+
@@firpm_mtime = nil
|
30
|
+
|
31
|
+
def firpm options
|
32
|
+
out = nil
|
33
|
+
File.open(FIRPM_CACHE_FILENAME, File::CREAT|File::RDWR) do |f|
|
34
|
+
f.flock(File::LOCK_EX)
|
35
|
+
if f.mtime != @@firpm_mtime
|
36
|
+
@@firpm_cache = YAML::load(f)
|
37
|
+
unless @@firpm_cache and @@firpm_cache[:version] == FIRPM_CACHE_VERSION
|
38
|
+
@@firpm_cache = {version:FIRPM_CACHE_VERSION}
|
39
|
+
end
|
40
|
+
f.seek 0
|
41
|
+
@@firpm_mtime = f.mtime
|
42
|
+
end
|
43
|
+
firpm = FirPM.new(options)
|
44
|
+
out = @@firpm_cache[firpm.options]
|
45
|
+
unless out
|
46
|
+
out = firpm.to_a
|
47
|
+
@@firpm_cache[firpm.options] = out
|
48
|
+
f.truncate 0
|
49
|
+
YAML::dump @@firpm_cache, f
|
50
|
+
end
|
51
|
+
end
|
52
|
+
out
|
53
|
+
end
|
54
|
+
module_function :firpm
|
55
|
+
|
56
|
+
# Instances of FirPM will act as the result array. The result
|
57
|
+
# is lazily generated just-in-time for the first use. This
|
58
|
+
# allows for CONSTANT=FirPM.new assignments without the huge
|
59
|
+
# penalty to application startup. It also let's us normalize
|
60
|
+
# the options at initialization for use as a cache key.
|
61
|
+
# It is strongly recommended to use the caching mixin.
|
62
|
+
class FirPM
|
63
|
+
|
64
|
+
# :type may be in [:bandpass, :differentiator, :hilbert]
|
65
|
+
# :maxiterations won't raise error when negative
|
66
|
+
def initialize options
|
67
|
+
# normalized and frozen to be suitable for a hash key
|
68
|
+
@options = {
|
69
|
+
type: options[:type].to_sym,
|
70
|
+
numtaps: options[:numtaps].to_i,
|
71
|
+
bands: options[:bands].flatten.collect(&:to_f).freeze,
|
72
|
+
desired: options[:desired].collect(&:to_f).freeze,
|
73
|
+
weights: options[:weights].collect(&:to_f).freeze,
|
74
|
+
griddensity: options[:griddensity] || 16,
|
75
|
+
maxiterations: options[:maxiterations] || 40
|
76
|
+
}.freeze
|
77
|
+
end
|
78
|
+
attr_reader :options
|
79
|
+
|
80
|
+
def method_missing name, *opts, &block
|
81
|
+
firpm unless @h
|
82
|
+
@h.send name, *opts, &block
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def firpm
|
88
|
+
numtaps = @options[:numtaps]
|
89
|
+
bands = @options[:bands]
|
90
|
+
des = @options[:desired]
|
91
|
+
weight = @options[:weights]
|
92
|
+
type = @options[:type]
|
93
|
+
griddensity = @options[:griddensity]
|
94
|
+
maxiterations = @options[:maxiterations]
|
95
|
+
|
96
|
+
numband = weight.size
|
97
|
+
unless bands.size == numband * 2 and des.size == numband * 2
|
98
|
+
raise 'size mismatch in bands, desired, or weights'
|
99
|
+
end
|
100
|
+
|
101
|
+
symmetry = (type == :bandpass) ? :positive : :negative
|
102
|
+
|
103
|
+
@r = numtaps / 2
|
104
|
+
@r += 1 if numtaps.odd? and symmetry == :positive
|
105
|
+
|
106
|
+
@gridsize = 0
|
107
|
+
numband.times do |i|
|
108
|
+
increment = 2.0 * @r * griddensity * (bands[2*i+1] - bands[2*i])
|
109
|
+
@gridsize += increment.round
|
110
|
+
end
|
111
|
+
@gridsize -= 1 if symmetry == :negative
|
112
|
+
|
113
|
+
taps = Array.new @r+1, 0.0
|
114
|
+
@h = Array.new numtaps, 0.0
|
115
|
+
@grid = Array.new @gridsize, 0.0
|
116
|
+
@d = Array.new @gridsize, 0.0
|
117
|
+
@w = Array.new @gridsize, 0.0
|
118
|
+
@e = Array.new @gridsize, 0.0
|
119
|
+
@ext = Array.new @r+1, 0
|
120
|
+
@x = Array.new @r+1, 0.0
|
121
|
+
@y = Array.new @r+1, 0.0
|
122
|
+
@ad = Array.new @r+1, 0.0
|
123
|
+
@foundExt = Array.new @r*2, 0
|
124
|
+
|
125
|
+
lowf = delf = 0.5/(griddensity*@r)
|
126
|
+
lowf = bands[0] unless symmetry == :negative and delf > bands[0]
|
127
|
+
j=0
|
128
|
+
numband.times do |band|
|
129
|
+
@grid[j] = bands[2*band]
|
130
|
+
lowf = bands[2*band] unless band == 0
|
131
|
+
highf = bands[2*band + 1]
|
132
|
+
k = ((highf - lowf)/delf).round
|
133
|
+
k.times do |i|
|
134
|
+
@d[j] = des[2*band] + i*(des[2*band+1]-des[2*band])/(k-1)
|
135
|
+
@w[j] = weight[band]
|
136
|
+
@grid[j] = lowf
|
137
|
+
lowf += delf
|
138
|
+
j += 1
|
139
|
+
end
|
140
|
+
@grid[j-1] = highf
|
141
|
+
end
|
142
|
+
@grid[@gridsize-1] = 0.5-delf if (
|
143
|
+
(symmetry == :negative) &&
|
144
|
+
(@grid[@gridsize-1] > (0.5 - delf)) &&
|
145
|
+
numtaps.odd?
|
146
|
+
)
|
147
|
+
|
148
|
+
(0..@r).each do |i|
|
149
|
+
@ext[i] = i * (@gridsize-1) / @r
|
150
|
+
end
|
151
|
+
|
152
|
+
if type == :differentiator
|
153
|
+
@gridsize.times do |i|
|
154
|
+
@w[i] = @w[i]/@grid[i] if @d[i] > 0.0001
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
if symmetry == :positive
|
159
|
+
if numtaps.even?
|
160
|
+
@gridsize.times do |i|
|
161
|
+
c = Math.cos(PI * @grid[i])
|
162
|
+
@d[i] /= c
|
163
|
+
@w[i] *= c
|
164
|
+
end
|
165
|
+
end
|
166
|
+
else
|
167
|
+
if numtaps.odd?
|
168
|
+
@gridsize.times do |i|
|
169
|
+
c = Math.sin(PI2 * @grid[i])
|
170
|
+
@d[i] /= c
|
171
|
+
@w[i] *= c
|
172
|
+
end
|
173
|
+
else
|
174
|
+
@gridsize.times do |i|
|
175
|
+
c = Math.sin(PI * @grid[i])
|
176
|
+
@d[i] /= c
|
177
|
+
@w[i] *= c
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
if maxiterations > 0
|
183
|
+
maxiter_error = true
|
184
|
+
else
|
185
|
+
maxiterations = -maxiterations
|
186
|
+
maxiter_error = false
|
187
|
+
end
|
188
|
+
iter = 0
|
189
|
+
while iter < maxiterations
|
190
|
+
calc_params
|
191
|
+
@gridsize.times do |i|
|
192
|
+
@e[i] = @w[i] * (@d[i] - compute_a(@grid[i]))
|
193
|
+
end
|
194
|
+
search
|
195
|
+
break if done?
|
196
|
+
iter += 1
|
197
|
+
end
|
198
|
+
raise "Maximum iterations exceeded" if maxiter_error && iter == maxiterations
|
199
|
+
calc_params
|
200
|
+
|
201
|
+
(0..numtaps/2).each do |i|
|
202
|
+
if symmetry == :positive
|
203
|
+
if numtaps.odd?
|
204
|
+
c = 1
|
205
|
+
else
|
206
|
+
c = Math.cos(PI * i / numtaps)
|
207
|
+
end
|
208
|
+
else
|
209
|
+
if numtaps.odd?
|
210
|
+
c = Math.sin(PI2 * i / numtaps)
|
211
|
+
else
|
212
|
+
c = Math.sin(PI * i / numtaps)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
taps[i] = compute_a(i.to_f / numtaps) * c
|
216
|
+
end
|
217
|
+
|
218
|
+
m = (numtaps.to_f-1)/2
|
219
|
+
if symmetry == :positive
|
220
|
+
if numtaps.odd?
|
221
|
+
(0...numtaps).each do |n|
|
222
|
+
val = taps[0]
|
223
|
+
x = PI2 * (n - m)/numtaps
|
224
|
+
(1..m).each do |k|
|
225
|
+
val += 2.0 * taps[k] * Math.cos(x*k)
|
226
|
+
end
|
227
|
+
@h[n] = val/numtaps
|
228
|
+
end
|
229
|
+
else
|
230
|
+
(0...numtaps).each do |n|
|
231
|
+
val = taps[0]
|
232
|
+
x = PI2 * (n - m)/numtaps
|
233
|
+
(1..numtaps/2-1).each do |k|
|
234
|
+
val += 2.0 * taps[k] * Math.cos(x*k)
|
235
|
+
end
|
236
|
+
@h[n] = val/numtaps
|
237
|
+
end
|
238
|
+
end
|
239
|
+
else
|
240
|
+
if numtaps.odd?
|
241
|
+
(0...numtaps).each do |n|
|
242
|
+
val = 0
|
243
|
+
x = PI2 * (n - m)/numtaps
|
244
|
+
(1..m).each do |k|
|
245
|
+
val += 2.0 * taps[k] * Math.sin(x*k)
|
246
|
+
end
|
247
|
+
@h[n] = val/numtaps
|
248
|
+
end
|
249
|
+
else
|
250
|
+
(0...numtaps).each do |n|
|
251
|
+
val = taps[numtaps/2] * Math.sin(PI * (n - m))
|
252
|
+
x = PI2 * (n - m) / numtaps
|
253
|
+
(1..numtaps/2-1).each do |k|
|
254
|
+
val += 2.0 * taps[k] * Math.sin(x*k)
|
255
|
+
end
|
256
|
+
@h[n] = val/numtaps
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
@gridsize = nil
|
262
|
+
@r = nil
|
263
|
+
@grid = nil
|
264
|
+
@d = nil
|
265
|
+
@w = nil
|
266
|
+
@e = nil
|
267
|
+
@ext = nil
|
268
|
+
@x = nil
|
269
|
+
@y = nil
|
270
|
+
@ad = nil
|
271
|
+
@foundExt = nil
|
272
|
+
@h.freeze
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
def calc_params
|
277
|
+
(0..@r).each do |i|
|
278
|
+
@x[i] = Math.cos(PI2 * @grid[@ext[i]])
|
279
|
+
end
|
280
|
+
ld = (@r-1)/15 + 1
|
281
|
+
(0..@r).each do |i|
|
282
|
+
denom = 1.0
|
283
|
+
xi = @x[i]
|
284
|
+
(0...ld).each do |j|
|
285
|
+
k=j
|
286
|
+
while k <= @r
|
287
|
+
denom *= 2.0*(xi - @x[k]) if k != i
|
288
|
+
k += ld
|
289
|
+
end
|
290
|
+
end
|
291
|
+
denom = 0.00001 if denom.abs < 0.00001
|
292
|
+
@ad[i] = 1.0/denom
|
293
|
+
end
|
294
|
+
numer = denom = 0
|
295
|
+
sign = 1
|
296
|
+
(0..@r).each do |i|
|
297
|
+
numer += @ad[i] * @d[@ext[i]]
|
298
|
+
denom += sign * @ad[i]/@w[@ext[i]]
|
299
|
+
sign = -sign
|
300
|
+
end
|
301
|
+
delta = numer/denom
|
302
|
+
sign = 1
|
303
|
+
(0..@r).each do |i|
|
304
|
+
@y[i] = @d[@ext[i]] - sign * delta / @w[@ext[i]]
|
305
|
+
sign = -sign
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
|
310
|
+
def compute_a freq
|
311
|
+
denom = numer = 0
|
312
|
+
xc = Math.cos(PI2 * freq)
|
313
|
+
(0..@r).each do |i|
|
314
|
+
c = xc - @x[i]
|
315
|
+
if c.abs < 1.0e-7
|
316
|
+
numer = @y[i]
|
317
|
+
denom = 1
|
318
|
+
break
|
319
|
+
end
|
320
|
+
c = @ad[i]/c
|
321
|
+
denom += c
|
322
|
+
numer += c*@y[i]
|
323
|
+
end
|
324
|
+
numer/denom
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
def search
|
329
|
+
@foundExt.fill 0
|
330
|
+
k = 0
|
331
|
+
if ((@e[0]>0.0) && (@e[0]>@e[1])) || ((@e[0]<0.0) && (@e[0]<@e[1]))
|
332
|
+
@foundExt[k] = 0
|
333
|
+
k += 1
|
334
|
+
end
|
335
|
+
(1...@gridsize-1).each do |i|
|
336
|
+
if (((@e[i]>=@e[i-1]) && (@e[i]>@e[i+1]) && (@e[i]>0.0)) ||
|
337
|
+
((@e[i]<=@e[i-1]) && (@e[i]<@e[i+1]) && (@e[i]<0.0)))
|
338
|
+
@foundExt[k] = i
|
339
|
+
k += 1
|
340
|
+
end
|
341
|
+
end
|
342
|
+
j = @gridsize-1
|
343
|
+
if (((@e[j]>0.0) && (@e[j]>@e[j-1])) ||
|
344
|
+
((@e[j]<0.0) && (@e[j]<@e[j-1])))
|
345
|
+
@foundExt[k] = j
|
346
|
+
k += 1
|
347
|
+
end
|
348
|
+
extra = k - (@r+1)
|
349
|
+
while (extra > 0)
|
350
|
+
up = @e[@foundExt[0]] > 0.0
|
351
|
+
l = 0
|
352
|
+
alt = true
|
353
|
+
(1...k).each do |j|
|
354
|
+
l = j if (@e[@foundExt[j]].abs < @e[@foundExt[l]].abs)
|
355
|
+
if up && (@e[@foundExt[j]] < 0.0)
|
356
|
+
up = false
|
357
|
+
elsif !up && (@e[@foundExt[j]] > 0.0)
|
358
|
+
up = true
|
359
|
+
else
|
360
|
+
alt = false
|
361
|
+
break
|
362
|
+
end
|
363
|
+
end
|
364
|
+
if alt && (extra == 1)
|
365
|
+
if (@e[@foundExt[k-1]].abs < @e[@foundExt[0]].abs)
|
366
|
+
l = @foundExt[k-1]
|
367
|
+
else
|
368
|
+
l = @foundExt[0]
|
369
|
+
end
|
370
|
+
end
|
371
|
+
(l...k).each do |j|
|
372
|
+
@foundExt[j] = @foundExt[j+1]
|
373
|
+
end
|
374
|
+
k -= 1
|
375
|
+
extra -= 1
|
376
|
+
end
|
377
|
+
(0..@r).each do |i|
|
378
|
+
@ext[i] = @foundExt[i]
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
def done?
|
384
|
+
min = max = @e[@ext[0]].abs
|
385
|
+
(1..@r).each do |i|
|
386
|
+
current = @e[@ext[i]].abs
|
387
|
+
min = current if current < min
|
388
|
+
max = current if current > max
|
389
|
+
end
|
390
|
+
((max-min)/max) < 0.0001
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|