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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +33 -0
  4. data/ext/roctave/cu8_file_reader.c +331 -0
  5. data/ext/roctave/cu8_file_reader.h +30 -0
  6. data/ext/roctave/extconf.rb +6 -0
  7. data/ext/roctave/fir_filter.c +795 -0
  8. data/ext/roctave/fir_filter.h +29 -0
  9. data/ext/roctave/freq_shifter.c +410 -0
  10. data/ext/roctave/freq_shifter.h +29 -0
  11. data/ext/roctave/iir_filter.c +462 -0
  12. data/ext/roctave/iir_filter.h +29 -0
  13. data/ext/roctave/roctave.c +38 -0
  14. data/ext/roctave/roctave.h +27 -0
  15. data/lib/roctave.rb +168 -0
  16. data/lib/roctave/bilinear.rb +92 -0
  17. data/lib/roctave/butter.rb +87 -0
  18. data/lib/roctave/cheby.rb +180 -0
  19. data/lib/roctave/cu8_file_reader.rb +45 -0
  20. data/lib/roctave/dft.rb +280 -0
  21. data/lib/roctave/filter.rb +64 -0
  22. data/lib/roctave/finite_difference_coefficients.rb +73 -0
  23. data/lib/roctave/fir.rb +121 -0
  24. data/lib/roctave/fir1.rb +134 -0
  25. data/lib/roctave/fir2.rb +246 -0
  26. data/lib/roctave/fir_design.rb +311 -0
  27. data/lib/roctave/firls.rb +380 -0
  28. data/lib/roctave/firpm.rb +499 -0
  29. data/lib/roctave/freq_shifter.rb +47 -0
  30. data/lib/roctave/freqz.rb +233 -0
  31. data/lib/roctave/iir.rb +80 -0
  32. data/lib/roctave/interp1.rb +78 -0
  33. data/lib/roctave/plot.rb +748 -0
  34. data/lib/roctave/poly.rb +46 -0
  35. data/lib/roctave/roots.rb +73 -0
  36. data/lib/roctave/sftrans.rb +157 -0
  37. data/lib/roctave/version.rb +3 -0
  38. data/lib/roctave/window.rb +116 -0
  39. data/roctave.gemspec +79 -0
  40. data/samples/butter.rb +12 -0
  41. data/samples/cheby.rb +28 -0
  42. data/samples/dft.rb +18 -0
  43. data/samples/differentiator.rb +48 -0
  44. data/samples/differentiator_frequency_scaling.rb +52 -0
  45. data/samples/fft.rb +40 -0
  46. data/samples/finite_difference_coefficient.rb +53 -0
  47. data/samples/fir1.rb +13 -0
  48. data/samples/fir2.rb +14 -0
  49. data/samples/fir2_windows.rb +29 -0
  50. data/samples/fir2bank.rb +30 -0
  51. data/samples/fir_low_pass.rb +44 -0
  52. data/samples/firls.rb +77 -0
  53. data/samples/firpm.rb +78 -0
  54. data/samples/hilbert_transformer.rb +20 -0
  55. data/samples/hilbert_transformer_frequency_scaling.rb +47 -0
  56. data/samples/plot.rb +45 -0
  57. data/samples/stem.rb +8 -0
  58. data/samples/type1.rb +25 -0
  59. data/samples/type3.rb +24 -0
  60. data/samples/windows.rb +25 -0
  61. metadata +123 -0
@@ -0,0 +1,121 @@
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 FirFilter
21
+ include Roctave::Filter
22
+
23
+ # @see Roctave.freqz
24
+ def freqz (*args)
25
+ Roctave.freqz(numerator, *args)
26
+ end
27
+
28
+ # @see Roctave.stem
29
+ def stem (*args)
30
+ Roctave.stem(numerator, *args)
31
+ end
32
+
33
+ # @see Roctave.zplane
34
+ def zplane
35
+ Roctave.zplane(numerator)
36
+ end
37
+
38
+ # @return [Roctave::FirFilter]
39
+ def clone
40
+ Roctave::FirFilter.new numerator
41
+ end
42
+
43
+ # Matlab-like +fir1+ function to generate a FIR filter.
44
+ # @see Roctave.fir1
45
+ # @return [Roctave::FirFilter]
46
+ def self.fir1 (*args)
47
+ Roctave::FirFilter.new Roctave.fir1(*args)
48
+ end
49
+
50
+ # Matlab-like +fir2+ function to generate a FIR filter.
51
+ # @see Roctave.fir2
52
+ # @return [Roctave::FirFilter]
53
+ def self.fir2 (*args)
54
+ Roctave::FirFilter.new Roctave.fir2(*args)
55
+ end
56
+
57
+ # Returns a low-pass FIR filter.
58
+ # @see Roctave.fir_low_pass
59
+ # @return [Roctave::FirFilter]
60
+ def self.low_pass (*args)
61
+ Roctave::FirFilter.new Roctave.fir_low_pass(*args)
62
+ end
63
+
64
+ # Returns a high-pass FIR filter.
65
+ # @see Roctave.fir_high_pass
66
+ # @return [Roctave::FirFilter]
67
+ def self.high_pass (*args)
68
+ Roctave::FirFilter.new Roctave.fir_high_pass(*args)
69
+ end
70
+
71
+ # Returns a band-pass FIR filter.
72
+ # @see Roctave.fir_band_pass
73
+ # @return [Roctave::FirFilter]
74
+ def self.band_pass (*args)
75
+ Roctave::FirFilter.new Roctave.fir_band_pass(*args)
76
+ end
77
+
78
+ # Returns a band-stop FIR filter.
79
+ # @see Roctave.fir_band_stop
80
+ # @return [Roctave::FirFilter]
81
+ def self.band_stop (*args)
82
+ Roctave::FirFilter.new Roctave.fir_band_stop(*args)
83
+ end
84
+
85
+ # Returns a Hilbert transformer FIR filter.
86
+ # @see Roctave.fir_hilbert
87
+ # @return [Roctave::FirFilter]
88
+ def self.hilbert (*args)
89
+ Roctave::FirFilter.new Roctave.fir_hilbert(*args)
90
+ end
91
+
92
+ # Returns a discriminator FIR filter.
93
+ # @see Roctave.fir_differentiator
94
+ # @return [Roctave::FirFilter]
95
+ def self.differentiator (*args)
96
+ Roctave::FirFilter.new Roctave.fir_differentiator(*args)
97
+ end
98
+
99
+ # Returns a kind of discriminator FIR filter.
100
+ # @see Roctave.finite_difference_coefficients
101
+ # @return [Roctave::FirFilter]
102
+ def self.finite_difference_coefficients (*args)
103
+ Roctave::FirFilter.new Roctave.finite_difference_coefficients(*args)
104
+ end
105
+
106
+ # Least square method to generate a FIR filter.
107
+ # @see Roctave.firls
108
+ # @return [Roctave::FirFilter]
109
+ def self.firls (*args)
110
+ Roctave::FirFilter.new Roctave.firls(*args)
111
+ end
112
+
113
+ # Least Parks-McClellan method to generate a FIR filter.
114
+ # @see Roctave.firpm
115
+ # @return [Roctave::FirFilter]
116
+ def self.firpm (*args)
117
+ Roctave::FirFilter.new Roctave.firpm(*args)
118
+ end
119
+ end
120
+ end
121
+
@@ -0,0 +1,134 @@
1
+ ## Copyright (C) 2000 Paul Kienzle <pkienzle@users.sf.net> (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
+ # @!group FIR filters
22
+
23
+ def self.fir1 (n, w, *args, ramp: nil, window: :hamming)
24
+ raise ArgumentError.new "Expecting no more than 4 arguments" if args.length > 2
25
+
26
+ n = [1, n.to_i].max
27
+
28
+ if w.kind_of?(Range) then
29
+ w = [w.begin, w.end]
30
+ elsif w.kind_of?(Numeric) then
31
+ w = [w]
32
+ end
33
+ raise ArgumentError.new "W must be a scalar, an array or a range" unless w.kind_of?(Array)
34
+ w.collect! do |e|
35
+ if e.kind_of?(Range) then
36
+ [e.begin, e.end]
37
+ else
38
+ e
39
+ end
40
+ end
41
+ w = w.flatten
42
+
43
+ scale = 1
44
+ ftype = (w.length == 1) ? 1 : 0
45
+
46
+ args.each do |arg|
47
+ case arg
48
+ when Symbol
49
+ arg = arg.to_s.downcase.to_sym
50
+ when String
51
+ arg = arg.downcase.to_sym
52
+ else
53
+ raise ArgumentError.new "Not expecting a #{arg.class}"
54
+ end
55
+
56
+ case arg
57
+ when :low
58
+ ftype = 1
59
+ when :stop
60
+ ftype = 1
61
+ when :dc1
62
+ ftype = 1
63
+ when :high
64
+ ftype = 0
65
+ when :pass
66
+ ftype = 0
67
+ when :bandpass
68
+ ftype = 0
69
+ when :dc0
70
+ ftype = 0
71
+ when :scale
72
+ scale = 1
73
+ when :noscale
74
+ scale = 0
75
+ else
76
+ raise ArgumentError.new "Unexpected argument #{arg}"
77
+ end
78
+ end
79
+
80
+ ## Build response function according to fir2 requirements
81
+ bands = w.length + 1
82
+ f = Roctave.zeros(2*bands)
83
+ f[0] = 0.0
84
+ f[-1] = 1.0
85
+ w.each.with_index do |e, i|
86
+ j = 1+2*i
87
+ break if j >= 2*bands - 1
88
+ f[j] = e
89
+ end
90
+ w.each.with_index do |e, i|
91
+ j = 2+2*i
92
+ break if j >= 2*bands - 1
93
+ f[j] = e
94
+ end
95
+ m = Roctave.zeros(2*bands)
96
+ r = (1..bands).collect{|e| (e - (1-ftype)) % 2}
97
+ r.each.with_index do |e, i|
98
+ j = 2*i
99
+ break if j >= 2*bands
100
+ m[j] = e
101
+ end
102
+ r = (0...2*bands).step(2).collect{|i| m[i]}
103
+ r.each.with_index do |e, i|
104
+ j = 1+2*i
105
+ break if j >= 2*bands
106
+ m[j] = e
107
+ end
108
+
109
+ if (n & 1) == 1 and m[-1].abs > 0.0 then
110
+ raise ArgumentError.new "Cannot do an odd order FIR filter with non-zero magnitude response at the nyquist frequency"
111
+ end
112
+
113
+ b = Roctave.fir2(n, f, m, ramp: ramp, window: window)
114
+
115
+ if scale == 1 then
116
+ if m[0] == 1 then
117
+ w_o = 0.0
118
+ elsif f[3] == 1 then
119
+ w_o = 1.0
120
+ else
121
+ w_o = f[2] + (f[3] - f[2]) / 2.0
122
+ end
123
+ renorm = 1.0 / b.collect.with_index{ |e, i|
124
+ j = b.length - 1 - i
125
+ e * (Complex.polar(1.0, -Math::PI*w_o)**j)
126
+ }.inject(&:+).abs
127
+ #puts "w_o: #{w_o}, renorm: #{renorm}"
128
+ b.collect!{|e| e * renorm}
129
+ end
130
+
131
+ b
132
+ end
133
+ end
134
+
@@ -0,0 +1,246 @@
1
+ ## Copyright (C) 2000 Paul Kienzle <pkienzle@users.sf.net> (implementation in octave)
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
+ # @!group FIR filters
22
+
23
+ ##
24
+ # Frequency sampling-based FIR filter design.
25
+ #
26
+ # Produce an order N FIR filter with arbitrary frequency response M
27
+ # over frequency bands F, returning the N+1 filter coefficients in B.
28
+ # The vector F specifies the frequency band edges of the filter
29
+ # response and M specifies the magnitude response at each frequency.
30
+ #
31
+ # The vector F must be nondecreasing over the range [0,1], and the
32
+ # first and last elements must be 0 and 1, respectively. A
33
+ # discontinuous jump in the frequency response can be specified by
34
+ # duplicating a band edge in F with different values in M.
35
+ #
36
+ # The resolution over which the frequency response is evaluated can
37
+ # be controlled with the GRID_N argument. The default is 512 or the
38
+ # next larger power of 2 greater than the filter length.
39
+ #
40
+ # The band transition width for discontinuities can be controlled
41
+ # with the RAMP_N argument. The default is GRID_N/25. Larger values
42
+ # will result in wider band transitions but better stopband
43
+ # rejection.
44
+ #
45
+ # An optional shaping WINDOW can be given as a vector with length
46
+ # N+1, or a symbol (ex: :blackman).
47
+ # If not specified, a Hamming window of length N+1 is used.
48
+ # @param n [Integer] The filter order
49
+ # @param f [Array<Float>] Frequency bands
50
+ # @param m [Array<Numeric>] Magnitude at frequency bands, can be complex numbers.
51
+ # @param type [nil, :odd_symmetry, :even_symmetry] Specify the symmetry of the filter. nil and :even_symmetry are the same and default, for type 1 and 2 filters, :odd_symmetry is for type 3 and 4 filters.
52
+ # @param grid [Integer] The length of the grid over which the frequency response is evaluated. Defaults to 512 or the next larger power of 2 greater than the filter length.
53
+ # @param ramp [Integer, Float] The band transition width for discontinuities, expressed in percentage relative to the niquist frequency.
54
+ # @param window [Array<Float>, Symbol] The window to use, either an array of length N+1, or a symbol. Defaults to :hamming
55
+ # @return [Array<Float>] The filter coefficients B
56
+ def self.fir2 (n, f, m, type = nil, grid: nil, ramp: nil, window: :hamming)
57
+ ## Taken from Octave /usr/share/octave/packages/signal-1.3.2/fir2.m
58
+ f = f.map{|e| e}
59
+ m = m.map{|e| e}
60
+ n = [1, n.to_i].max
61
+ len = n + 1
62
+
63
+ raise ArgumentError.new "Argument 2 must be an array" unless f.kind_of?(Array)
64
+ raise ArgumentError.new "Argument 3 must be an array" unless m.kind_of?(Array)
65
+ case type
66
+ when nil
67
+ type = :even_symmetry
68
+ when :even_symmetry
69
+ when :odd_symmetry
70
+ else
71
+ raise ArgumentError.new "Argument 4 (type) must either be nil, :odd_symmetry or :even_symmetry" unless m.kind_of?(Array)
72
+ end
73
+
74
+ case window
75
+ when Array
76
+ raise ArgumentError.new "Window array must be of length #{len}" unless window.length == len
77
+ when Symbol
78
+ window = self.send(window, len)
79
+ when nil
80
+ else
81
+ raise ArgumentError.new "Window must either be an array of length N+1 or the symbol of a window"
82
+ end
83
+
84
+ if grid then
85
+ grid = [((n+1)/2.0).ceil.to_i, grid.to_i].max
86
+ else
87
+ grid = 512
88
+ grid = (2**Math.log2(len).ceil).to_i if grid < len
89
+ end
90
+
91
+ if ramp then
92
+ ramp = ramp.abs
93
+ else
94
+ ramp = 4
95
+ end
96
+ ramp = ramp.to_f / 200.0
97
+
98
+ if f.length < 2 or f[0] != 0.0 or f[-1] != 1.0 or (1 ... f.length).collect{|i| (f[i] - f[i-1]) < 0.0}.include?(true) then
99
+ raise ArgumentError.new "frequency must be nondecreasing starting from 0 and ending at 1"
100
+ elsif f.length != m.length then
101
+ raise ArgumentError.new "frequency and magnitude arrays must be the same length"
102
+ end
103
+
104
+
105
+ if type == :odd_symmetry and m.find{|v| v.is_a?(Complex)}.nil? then
106
+ m.collect!{|v| Complex(0, -v)}
107
+ end
108
+
109
+
110
+ ## Apply ramps to discontinuities
111
+ if ramp > 0 then
112
+ ## Remember the original frequency points prior to applying ramp
113
+ basef = f.collect{|e| e}
114
+ basem = m.collect{|e| e}
115
+
116
+ ## Separate identical frequencies, but keep the midpoints
117
+ idx = (1 ... f.length).collect{|i| ((f[i] - f[i-1]) == 0.0) ? i-1 : nil}.reject(&:nil?)
118
+ idx.each do |i|
119
+ f[i] -= ramp
120
+ f[i+1] += ramp
121
+ end
122
+ ## Make sure the frequency points stay monotonic in [0, 1]
123
+ f = (f + basef).collect{|e| [1.0, [0.0, e].max].min}.uniq.sort
124
+
125
+ ## Preserve window shape even though f may have changed
126
+ m = f.collect.with_index do |framped, i|
127
+ leftmost = nil
128
+ leftmost_index = nil
129
+ basef.each.with_index do |e, j|
130
+ if (e < framped) or (e == framped and leftmost != framped) then
131
+ leftmost = e
132
+ leftmost_index = j
133
+ end
134
+ end
135
+
136
+ rightmost = nil
137
+ rightmost_index = nil
138
+ basef.reverse.each.with_index do |e, j|
139
+ if (e > framped) or (e == framped and rightmost != framped) then
140
+ rightmost = e
141
+ rightmost_index = basef.length - 1 - j
142
+ end
143
+ end
144
+
145
+ if leftmost_index > rightmost_index then
146
+ leftmost,rightmost = rightmost,leftmost
147
+ leftmost_index,rightmost_index = rightmost_index,leftmost_index
148
+ end
149
+
150
+ #puts "[#{i}] #{leftmost_index} - #{rightmost_index}"
151
+
152
+ ml = basem[leftmost_index]
153
+ mr = basem[rightmost_index]
154
+
155
+ if (rightmost - leftmost) == 0 then
156
+ (ml + mr) / 2.0
157
+ else
158
+ ml*(rightmost - framped)/(rightmost - leftmost) + mr*(framped - leftmost)/(rightmost - leftmost)
159
+ end
160
+ end
161
+ #Roctave.plot(basef, basem, '-or;original;', f, m, '-*b;ramped;', xlim: (-0.1 .. 1.1), ylim: (-0.1 .. 1.1), grid: true, title: 'Ramp')
162
+ end
163
+
164
+ ## Interpolate between grid points
165
+ fgrid = Roctave.linspace(0.0, 1.0, grid + 1)
166
+ mgrid = fgrid.collect.with_index do |fg, i|
167
+ leftmost = nil
168
+ leftmost_index = nil
169
+ f.each.with_index do |e, j|
170
+ if (e < fg) or (e == fg and leftmost != e) then
171
+ leftmost = e
172
+ leftmost_index = j
173
+ end
174
+ end
175
+
176
+ rightmost = nil
177
+ rightmost_index = nil
178
+ f.reverse.each.with_index do |e, j|
179
+ if (e > fg) or (e == fg and rightmost != e) then
180
+ rightmost = e
181
+ rightmost_index = f.length - 1 - j
182
+ end
183
+ end
184
+
185
+ if leftmost_index > rightmost_index then
186
+ leftmost,rightmost = rightmost,leftmost
187
+ leftmost_index,rightmost_index = rightmost_index,leftmost_index
188
+ end
189
+
190
+ ml = m[leftmost_index]
191
+ mr = m[rightmost_index]
192
+
193
+ if (rightmost - leftmost) == 0 then
194
+ (ml + mr) / 2.0
195
+ else
196
+ ml*(rightmost - fg)/(rightmost - leftmost) + mr*(fg - leftmost)/(rightmost - leftmost)
197
+ end
198
+ end
199
+
200
+
201
+ ## Transform frequency response into time response and
202
+ ## center the response about n/2, truncating the excess
203
+ if type == :even_symmetry then
204
+ if (n & 1) == 0 then # Type I
205
+ #puts 'Type I'
206
+ b = mgrid + mgrid[1...grid].reverse
207
+ b = idft(b)
208
+ mid = (n+1) / 2.0
209
+ b = (b[-(mid.floor) .. -1] + b[0 ... mid.ceil]).collect{|e| e.real}
210
+ else # Type II
211
+ #puts 'Type II'
212
+ ## Add zeros to interpolate by 2, then pick the odd values below
213
+ b = mgrid + Array.new(grid*2){0.0} + mgrid[1 ... grid].reverse
214
+ b = idft(b)
215
+ b = (((b.length - n) ... b.length).step(2).to_a + (1 .. n).step(2).to_a).collect{|i| b[i].real * 2.0}
216
+ end
217
+ else
218
+ if (n & 1) == 0 then # Type III
219
+ #puts 'Type III'
220
+ b = mgrid + mgrid[1...grid].conj.reverse
221
+ ####
222
+ #br = b.collect{|v| v.real}
223
+ #bi = b.collect{|v| v.imag}
224
+ #t = (-b.length/2...b.length/2).to_a
225
+ #plot(t, br, ';Real;', t, bi, ';Imaginary;')
226
+ ####
227
+ b = idft(b)
228
+ mid = (n+1) / 2.0
229
+ b = (b[-(mid.floor) .. -1] + b[0 ... mid.ceil]).collect{|e| e.real}
230
+ else # Type IV
231
+ #puts 'Type IV'
232
+ ## Add zeros to interpolate by 2, then pick the odd values below
233
+ b = mgrid + Array.new(grid*2){0.0} + mgrid.conj[1 ... grid].reverse
234
+ b = idft(b)
235
+ b = (((b.length - n) ... b.length).step(2).to_a + (1 .. n).step(2).to_a).collect{|i| b[i].real * 2.0}
236
+ end
237
+ end
238
+
239
+ b.collect!.with_index{|e, i| e*window[i]} if window
240
+
241
+ #Roctave.plot(f, m, '-or;original;', fgrid, mgrid, '-xb;grid;', Roctave.linspace(0, 1, 1024), Roctave.dft(b, 2048).abs[0...1024], 'g;response;', xlim: (-0.1 .. 1.1), ylim: (-0.1 .. 1.1), grid: true, title: 'Grid')
242
+
243
+ b
244
+ end
245
+ end
246
+