digiproc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +48 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +78 -0
  10. data/Rakefile +37 -0
  11. data/TODO.md +50 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/config/environment.rb +118 -0
  15. data/console_tests.rb +44 -0
  16. data/digiproc.gemspec +49 -0
  17. data/examples/analog_signals/analog_to_digital.rb +31 -0
  18. data/examples/analog_signals/companded-signals.png +0 -0
  19. data/examples/analog_signals/companding.rb +68 -0
  20. data/examples/analog_signals/fft-plot.png +0 -0
  21. data/examples/analog_signals/plot_Digiproc::FFT.png +0 -0
  22. data/examples/analog_signals/plot_Dsp::FFT.png +0 -0
  23. data/examples/analog_signals/quantization-outputs.png +0 -0
  24. data/examples/analog_signals/quantize_compand.rb +69 -0
  25. data/examples/binomial_distribution/bit_error.rb +14 -0
  26. data/examples/binomial_distribution/dice.rb +35 -0
  27. data/examples/digital_signals/_coded_frequency_signal,_ts_=_1_s.png +0 -0
  28. data/examples/digital_signals/_coded_frequency_signal,_ts_=_2_s.png +0 -0
  29. data/examples/digital_signals/coded_power_spectral_density,__ts_=_1_s.png +0 -0
  30. data/examples/digital_signals/coded_power_spectral_density,__ts_=_2_s.png +0 -0
  31. data/examples/digital_signals/coded_time_signal,_ts_=_1_s.png +0 -0
  32. data/examples/digital_signals/coded_time_signal,_ts_=_2_s.png +0 -0
  33. data/examples/digital_signals/freq_sig_from_eqn,_ts_=_1_s.png +0 -0
  34. data/examples/digital_signals/freq_sig_from_eqn,_ts_=_2_s.png +0 -0
  35. data/examples/digital_signals/frequency_signal,_ts_=_1_s.png +0 -0
  36. data/examples/digital_signals/frequency_signal,_ts_=_2_s.png +0 -0
  37. data/examples/digital_signals/modulate_square_pulses.rb +9 -0
  38. data/examples/digital_signals/modulated_sq._pulses.png +0 -0
  39. data/examples/digital_signals/modulated_sq._pulses_alt.png +0 -0
  40. data/examples/digital_signals/power_spectral_density,__ts_=_1_s.png +0 -0
  41. data/examples/digital_signals/power_spectral_density,__ts_=_2_s.png +0 -0
  42. data/examples/digital_signals/square_signals.rb +90 -0
  43. data/examples/digital_signals/time_signal,_ts_=_1_s.png +0 -0
  44. data/examples/digital_signals/time_signal,_ts_=_2_s.png +0 -0
  45. data/examples/encoding/gray_code.rb +22 -0
  46. data/examples/encoding/psk.rb +91 -0
  47. data/examples/encoding/system_2_phase.png +0 -0
  48. data/examples/encoding/system_2_xmit_signal.png +0 -0
  49. data/examples/encoding/system_3_phase.png +0 -0
  50. data/examples/encoding/system_3_xmit_signal.png +0 -0
  51. data/examples/encoding/system_4_xmit_signal.png +0 -0
  52. data/examples/encoding/xor-dpsk-phase-signal-(sys1).png +0 -0
  53. data/examples/encoding/xor-dpsk-xmit-signal-(sys-1).png +0 -0
  54. data/examples/factories/Quickplot Graph.png +0 -0
  55. data/examples/factories/bandpass.rb +6 -0
  56. data/examples/fft/plot_Dsp::FFT.png +0 -0
  57. data/examples/fft/recieved_data_(time_domain).png +0 -0
  58. data/examples/fft/simple_fft_example.rb +47 -0
  59. data/examples/fft/unprocessed_fft.png +0 -0
  60. data/examples/filters/bandpass_filter.png +0 -0
  61. data/examples/filters/filter_a_signal.rb +38 -0
  62. data/examples/filters/white_noise_db_out_of_bp_filter.png +0 -0
  63. data/examples/filters/white_noise_mag_out_of_bp_filter.png +0 -0
  64. data/examples/filters/white_noise_spectra.png +0 -0
  65. data/examples/functions/compute_probability.rb +29 -0
  66. data/examples/functions/gram_schmidt.rb +10 -0
  67. data/examples/functions/minimize_energy.rb +29 -0
  68. data/examples/functions/orthoganalize.rb +18 -0
  69. data/examples/functions/simple_functions.rb +81 -0
  70. data/examples/linear_algebra/diverging_sys.rb +13 -0
  71. data/examples/linear_algebra/iterative_sys_of_eqns_methods.rb +27 -0
  72. data/examples/modulation_schemes/dpsk_2.png +0 -0
  73. data/examples/modulation_schemes/dpsk_256.png +0 -0
  74. data/examples/modulation_schemes/dpsk_freq_domain.rb +119 -0
  75. data/examples/modulation_schemes/psk.rb +36 -0
  76. data/examples/modulation_schemes/psk_2.png +0 -0
  77. data/examples/modulation_schemes/psk_256.png +0 -0
  78. data/examples/modulation_schemes/psksystem_1_xmit_signal.png +0 -0
  79. data/examples/modulation_schemes/psksystem_2_xmit_signal.png +0 -0
  80. data/examples/modulation_schemes/psksystem_3_xmit_signal.png +0 -0
  81. data/examples/modulation_schemes/system_1_xmit_signal.png +0 -0
  82. data/examples/modulation_schemes/system_2_xmit_signal.png +0 -0
  83. data/examples/modulation_schemes/system_3_xmit_signal.png +0 -0
  84. data/examples/quickplot/PlottableClass_plot.png +0 -0
  85. data/examples/quickplot/decorators.rb +13 -0
  86. data/examples/quickplot/direct_gruff.png +0 -0
  87. data/examples/quickplot/plot_PlottableClass.png +0 -0
  88. data/examples/quickplot/quickplot_vs_others.rb +85 -0
  89. data/examples/quickplot/random_data_quickplot,_dark.png +0 -0
  90. data/examples/quickplot/random_data_quickplot.png +0 -0
  91. data/examples/realized_gaussian/norm_dist_plot.png +0 -0
  92. data/examples/realized_gaussian/norm_dist_spectrum.png +0 -0
  93. data/examples/realized_gaussian/realized_gaussian_example.rb +23 -0
  94. data/lib/concerns/convolvable.rb +144 -0
  95. data/lib/concerns/data_properties.rb +223 -0
  96. data/lib/concerns/fourier_transformable.rb +178 -0
  97. data/lib/concerns/initializable.rb +43 -0
  98. data/lib/concerns/multipliable.rb +22 -0
  99. data/lib/concerns/os.rb +36 -0
  100. data/lib/concerns/plottable.rb +248 -0
  101. data/lib/concerns/requires_data.rb +8 -0
  102. data/lib/digiproc/version.rb +8 -0
  103. data/lib/digiproc.rb +2 -0
  104. data/lib/extensions/array_extension.rb +23 -0
  105. data/lib/extensions/core_extensions.rb +117 -0
  106. data/lib/factories/factories.rb +3 -0
  107. data/lib/factories/filter_factory.rb +83 -0
  108. data/lib/factories/window_factory.rb +22 -0
  109. data/lib/fft.rb +255 -0
  110. data/lib/filters/bandpass_filter.rb +43 -0
  111. data/lib/filters/bandstop_filter.rb +44 -0
  112. data/lib/filters/digital_filter.rb +59 -0
  113. data/lib/filters/highpass_filter.rb +27 -0
  114. data/lib/filters/lowpass_filter.rb +27 -0
  115. data/lib/functions.rb +221 -0
  116. data/lib/probability/binomial_distribution.rb +84 -0
  117. data/lib/probability/bit_generator.rb +94 -0
  118. data/lib/probability/gaussian_distribution.rb +29 -0
  119. data/lib/probability/probability.rb +234 -0
  120. data/lib/probability/theoretical_gaussian_distribution.rb +59 -0
  121. data/lib/quick_plot.rb +96 -0
  122. data/lib/rbplot.rb +219 -0
  123. data/lib/signals/analog_signal.rb +143 -0
  124. data/lib/signals/digital_signal.rb +181 -0
  125. data/lib/strategies/code/differential_encoding_strategy.rb +69 -0
  126. data/lib/strategies/code/gray_code.rb +75 -0
  127. data/lib/strategies/code/xor_differential_encoding_strategy.rb +100 -0
  128. data/lib/strategies/code/xor_differential_encoding_zero_angle_strategy.rb +103 -0
  129. data/lib/strategies/companding/custom_companding_strategy.rb +29 -0
  130. data/lib/strategies/convolution/bf_conv.rb +57 -0
  131. data/lib/strategies/fft/brute_force_dft_strategy.rb +31 -0
  132. data/lib/strategies/fft/inverse_fft_conjugate_strategy.rb +44 -0
  133. data/lib/strategies/fft/radix2_strategy.rb +84 -0
  134. data/lib/strategies/gaussian/gaussian_generator.rb +49 -0
  135. data/lib/strategies/linear_algebra/gauss_seidel_strategy.rb +90 -0
  136. data/lib/strategies/linear_algebra/jacobi_strategy.rb +81 -0
  137. data/lib/strategies/linear_algebra/sor2_strategy.rb +98 -0
  138. data/lib/strategies/linear_algebra/sor_strategy.rb +108 -0
  139. data/lib/strategies/modulation/phase_shift_keying_strategy.rb +96 -0
  140. data/lib/strategies/orthogonalize/gram_schmidt.rb +50 -0
  141. data/lib/strategies/strategies.rb +3 -0
  142. data/lib/strategies/window/blackman_window.rb +32 -0
  143. data/lib/strategies/window/hamming_window.rb +31 -0
  144. data/lib/strategies/window/hanning_window.rb +31 -0
  145. data/lib/strategies/window/kaiser_window.rb +27 -0
  146. data/lib/strategies/window/rectangular_window.rb +22 -0
  147. data/lib/strategies/window/window.rb +42 -0
  148. data/lib/systems/custom_system.rb +13 -0
  149. data/lib/systems/hilbert_transform.rb +6 -0
  150. data/lib/systems/matched_filter.rb +21 -0
  151. data/lib/systems/raised_cosine_filter.rb +11 -0
  152. data/lib/systems/system.rb +19 -0
  153. data/lib/systems/systems.rb +3 -0
  154. data/playground.rb +323 -0
  155. data/plots/_coded_frequency_signal,_ts_=_1_s.png +0 -0
  156. data/plots/_coded_frequency_signal,_ts_=_2_s.png +0 -0
  157. data/plots/coded_freq_sig_from_eqn,_ts_=_1_s.png +0 -0
  158. data/plots/coded_freq_sig_from_eqn,_ts_=_2_s.png +0 -0
  159. data/plots/coded_power_spectral_density,__ts_=_1_s.png +0 -0
  160. data/plots/coded_power_spectral_density,__ts_=_2_s.png +0 -0
  161. data/plots/coded_time_signal,_ts_=_1_s.png +0 -0
  162. data/plots/coded_time_signal,_ts_=_2_s.png +0 -0
  163. data/plots/dpsk_2.png +0 -0
  164. data/plots/freq_sig_from_eqn,_ts_=_1_s.png +0 -0
  165. data/plots/freq_sig_from_eqn,_ts_=_2_s.png +0 -0
  166. data/plots/frequency_signal,_ts_=_1_s.png +0 -0
  167. data/plots/frequency_signal,_ts_=_2_s.png +0 -0
  168. data/plots/power_spectral_density,__ts_=_1_s.png +0 -0
  169. data/plots/power_spectral_density,__ts_=_2_s.png +0 -0
  170. data/plots/psk_2.png +0 -0
  171. data/plots/time_signal,_ts_=_1_s.png +0 -0
  172. data/plots/time_signal,_ts_=_2_s.png +0 -0
  173. data/test-title-dark.png +0 -0
  174. data/test-title.png +0 -0
  175. metadata +322 -0
data/lib/fft.rb ADDED
@@ -0,0 +1,255 @@
1
+ ##
2
+ # Class to calculate and store the Discrete Fourier Transform of a siugnal
3
+ class Digiproc::FFT
4
+
5
+ ##
6
+ # Calculate the FFT of given time data
7
+ # == Input arg
8
+ # time_data:: Array[Numeric]
9
+ def self.calculate(time_data)
10
+ Radix2Strategy.calculate(time_data)
11
+ end
12
+
13
+ ##
14
+ # Calculate the IFFT of the given frequency data
15
+ # Input frequency data, perform the Inverse FFT to populate the time data
16
+ # == Input arg
17
+ # data:: Array[Numeric]
18
+ def self.new_from_spectrum(data)
19
+ time_data = Digiproc::Strategies::IFFTConjugateStrategy.new(data)
20
+ new(freq_data: data, time_data: time_data)
21
+ end
22
+
23
+ # Reader for @data
24
+ # Allows for lazy calculation of @data (which holds frequency domain data)
25
+ # If @data is nil, the #calculate method will be called
26
+ def data
27
+ calculate if @data.nil?
28
+ @data
29
+ end
30
+
31
+ attr_accessor :strategy, :window, :processed_time_data, :time_data_size, :inverse_strategy
32
+ include Digiproc::Convolvable::InstanceMethods, Digiproc::Plottable::InstanceMethods
33
+
34
+
35
+ ##
36
+ # == Input Args
37
+ # strategy:: FFT Strategy, see Digiproc::Strategies::Radix2Strategy to see required Protocol
38
+ # time_data:: Array[Numeric] time data to be transformed to the frequency domain via the FFT strategy
39
+ # size (Optional):: Integer, defaults to nil. If not set, your data will be zero padded to the closest higher power of 2 (for Radix2Strategy), or not changed at all
40
+ # window (Optional):: Digiproc::Window, defaults to Digiproc::RectangularWindow. Changes the window used during #process_with_window method
41
+ # freq_data (Optional):: Array[Niumeric], required if time_data not given
42
+ # inverse_strategy (Optional):: Digiproc::Strategies::IFFTConjugateStrategy is the default value and shows the required protocol
43
+ # Note: Using size with a Radix2Strategy will only ensure a minimum amount of
44
+ # zero-padding, it will mostly likely not determine the final size of the time_data
45
+ # You need to have EITHER time_data or freq_data, but not both.
46
+ def initialize(strategy: Digiproc::Strategies::Radix2Strategy, time_data: nil, size: nil, window: Digiproc::RectangularWindow, freq_data: nil, inverse_strategy: Digiproc::Strategies::IFFTConjugateStrategy)
47
+ raise ArgumentError.new("Either time or frequency data must be given") if time_data.nil? and freq_data.nil?
48
+ raise ArgumentError.new('Size must be an integer') if not size.nil? and not size.is_a?(Integer)
49
+ raise ArguemntError.new('Size must be greater than zero') if not size.nil? and size <= 0
50
+ raise ArgumentError.new('time_data must be an array') if not time_data.respond_to?(:calculate) and not time_data.is_a? Array
51
+
52
+ if time_data.is_a? Array
53
+ @time_data_size = time_data.length
54
+ if not size.nil?
55
+ if size <= time_data.length
56
+ @time_data = time_data.dup.map{ |val| val.dup }.take(size)
57
+ else
58
+ zero_fill = Array.new(size - time_data.length, 0)
59
+ @time_data = time_data.dup.map{ |val| val.dup }.concat zero_fill
60
+ end
61
+ else
62
+ @time_data = time_data.dup.map{ |val| val.dup}
63
+ end
64
+ @strategy = strategy.new(@time_data.map{ |val| val.dup})
65
+ @window = window.new(size: time_data_size)
66
+ else
67
+ @time_data = time_data
68
+ @strategy = strategy.new
69
+ @window = window.new(size: freq_data.length)
70
+ end
71
+ @inverse_strategy = inverse_strategy
72
+ @data = freq_data
73
+ end
74
+
75
+ ##
76
+ # Performs FFT caclulation if not yet performed. Returns FFT data as an Array of Floats (or an array of Complex numbers)
77
+ def calculate
78
+ self.strategy.data = time_data if @strategy.data.nil?
79
+ @fft = self.strategy.calculate
80
+ @data = @fft
81
+ end
82
+
83
+ ##
84
+ # Input argument of an Integer describing the required size of the FFT. IF using a strategy requiring a certain amount of
85
+ # data points (ie Radix2), you will be guaranteed tha the FFT is greater than or equal to the input size. Otherwise, your FFT will be this size
86
+ def calculate_at_size(size)
87
+ if size > self.data.size
88
+ zero_fill = Array.new(size - @time_data.length, 0)
89
+ @time_data = time_data.concat zero_fill
90
+ elsif size < self.data.size
91
+ @time_data = time_data.take(size)
92
+ end
93
+ self.strategy.data = time_data
94
+ calculate
95
+ end
96
+
97
+ ##
98
+ # Calculate the IFFT of the frequency data
99
+ def ifft
100
+ inverse_strategy.new(data).calculate
101
+ end
102
+
103
+ ##
104
+ # Calculate the IFFT and return it as a Digiproc::DigitalSignal
105
+ def ifft_ds
106
+ Digiproc::DigitalSignal.new(data: ifft)
107
+ end
108
+
109
+
110
+ ##
111
+ # Returns the time_data as an Array of Numerics (floats or Complex numbers)
112
+ def time_data
113
+ if @time_data.is_a? Array
114
+ @time_data
115
+ elsif @time_data.respond_to? :calculate
116
+ @time_data = @time_data.calculate
117
+ else
118
+ raise TypeError.new("time_data needs to be an array or an ifft strategy, not a #{@time_data.class}")
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Processes the time_data with the chosen window valuesand calculates the FFT based of of the window-processed time domain signals
124
+ def process_with_window
125
+ @processed_time_data = time_data.take(time_data_size).times self.window.values
126
+ self.strategy.data = @processed_time_data
127
+ @fft = self.strategy.calculate
128
+ @data = @fft
129
+ end
130
+
131
+ ##
132
+ # Returns the frequency domain data as an Array of Numerics (Float or Complex)
133
+ def fft
134
+ self.data
135
+ end
136
+
137
+ ##
138
+ # Return the number of frequency domain datapoints
139
+ def size
140
+ self.data.length
141
+ end
142
+
143
+ ##
144
+ # Return the magnitude of the frequency domain values as an array of floats
145
+ def magnitude
146
+ data.map do |f|
147
+ f.abs
148
+ end
149
+ end
150
+
151
+ ##
152
+ # Return the complex conjugate of the frequency domain data, as an array of numerics (float or complex)
153
+ def conjugate
154
+ self.data.map(&:conjugate)
155
+ end
156
+
157
+ ##
158
+ # Return the decible of the frequency domain data, as an Array of floats
159
+ def dB
160
+ self.magnitude.map do |m|
161
+ Math.db(m)
162
+ end
163
+ end
164
+
165
+ ##
166
+ # Returns the angle of the frequency domain data, as an array of floats (in radians)
167
+ def angle
168
+ self.data.map(&:angle)
169
+ end
170
+
171
+ ##
172
+ # Returns the real part of the frequency domain data, as an array of floats
173
+ def real
174
+ self.data.map(&:real)
175
+ end
176
+
177
+ ##
178
+ # Returns the imaginary part of the frequency domain data, as an array of floats
179
+ def imaginary
180
+ self.data.map(&:imaginary)
181
+ end
182
+
183
+ ##
184
+ # Returns the maximum value(s) of the magnitude of the frequency signal as an Array of OpenStructs with
185
+ # an index and value property.
186
+ # == Input arg
187
+ # num (Optional):: The number of maxima desired, defaults to 1
188
+ def maxima(num = 1)
189
+ Digiproc::DataProperties.maxima(self.magnitude, num)
190
+ end
191
+
192
+ ##
193
+ # Returns the local maximum value(s) of the magnitude of the frequency signal as an Array of OpenStructs with
194
+ # an index and value property.
195
+ # Local maxima are determined by Digiproc::DataProperties.local_maxima, and the returned maxima are determined based off of
196
+ # their relative hight to adjacent maxima. This is useful for looking for spikes in frequency data
197
+ # == Input arg
198
+ # num (Optional):: The number of maxima desired, defaults to 1
199
+ def local_maxima(num = 1)
200
+ Digiproc::DataProperties.local_maxima(self.magnitude, num)
201
+ end
202
+
203
+ ##
204
+ # Allows multioplication of FFT objects with anything with a @data reader which holds an Array of Numerics.
205
+ # The return value is a new FFT object whose frequency data is the element-by-element multiplication of the two data arrays
206
+ def *(obj)
207
+ if obj.respond_to?(:data)
208
+ return self.class.new_from_spectrum(self.data.times obj.data)
209
+ elsif obj.is_a? Array
210
+ return self.class.new_from_spectrum(self.data.times obj)
211
+ end
212
+ end
213
+
214
+ ##
215
+ # Uses Plottable module to plot the db values
216
+ def plot_db(path: "./")
217
+ self.plot(method: :dB, xsteps: 8, path: path) do |g|
218
+ g.title = "Decibles"
219
+ g.x_axis_label = "Normalized Frequency"
220
+ g.y_axis_label = "Magnitude"
221
+ end
222
+ end
223
+
224
+ ##
225
+ # uses Plottable module to plot the magnitude values
226
+ def plot_magnitude(path: "./" )
227
+ self.plot(method: :magnitude, xsteps: 8, path: path) do |g|
228
+ g.title = "Magnitude"
229
+ g.x_axis_label = "Normalized Frequency"
230
+ g.y_axis_label = "Magnitude"
231
+ end
232
+ end
233
+
234
+
235
+ ##
236
+ # TODO: Remove
237
+ # plots magnitude using Gruff directly
238
+ def graph_magnitude(file_name = "fft")
239
+ if @fft
240
+ g = Gruff::Line.new
241
+ g.data :fft, self.magnitude
242
+ g.write("./#{file_name}.png")
243
+ end
244
+ end
245
+
246
+ ##
247
+ # TODO: Remove
248
+ # Plots time data using Gruff directly
249
+ def graph_time_data
250
+ g = Gruff::Line.new
251
+ g.data :data, @time_data
252
+ end
253
+
254
+
255
+ end
@@ -0,0 +1,43 @@
1
+ ##
2
+ # Creates a Bandpass Filter via the Windowing method.
3
+ class Digiproc::BandpassFilter < Digiproc::DigitalFilter
4
+ attr_accessor :equation
5
+
6
+ ##
7
+ # == Inputs
8
+ # size:: [Integer] number of datapoints window should be
9
+ # window:: [Digiproc::WindowStrategy] desired window strategy
10
+ # wo:: [Float] center frequency in radians
11
+ # bw:: [Float] bandwidth in radians
12
+ # wcl:: [Float] lower cutoff frequency in radians
13
+ # wch:: [Float] higher cutoff frequency in radians
14
+ # correct:: [Boolean] perform frequency corrections to make frequency points more accurate. Defaults to true
15
+ #
16
+ # Must have either `wo` and `bw` or `wcl` and `wch`
17
+ #
18
+ ## Digiproc::BandpassFilter.new(size: 1000, wo: Math::PI / 4, bw: Math::PI / 10)
19
+
20
+ def initialize(size:, window: Digiproc::RectangularWindow, wo: nil, bw: nil, wcl: nil , wch: nil, correct: true )
21
+
22
+ super(size: size, window: window)
23
+
24
+ if !!wo && !!bw
25
+ bw += @window.transition_width * 2 * PI if correct
26
+ wcl = wo - bw / 2.0
27
+ wch = wo + bw / 2.0
28
+ else
29
+ raise ArgumentError.new("You must provide either bandwidth and center freq or frequency bands") if wcl.nil? or wch.nil?
30
+ wcl -= @window.transition_width * PI if correct
31
+ wch += @window.transition_width * PI if correct
32
+ bw = wch - wcl
33
+ wo = (wch + wcl) / 2.0
34
+ end
35
+ @equation = ->(n){
36
+ n == 0 ? bw / PI : ((Math.sin(bw * n / 2.0)) / (PI * n)) * (2.0 * Math.cos(n * wo))
37
+ }
38
+ ideal_filter = calculate_ideal
39
+ @weights = self.window.values.times ideal_filter
40
+ @fft = Digiproc::FFT.new(time_data: self.weights)
41
+ @fft.calculate
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+
2
+ ##
3
+ # Creates a bandstop filter via the Windowing Method
4
+
5
+ class Digiproc::BandstopFilter < Digiproc::DigitalFilter
6
+ attr_accessor :equation
7
+
8
+ ##
9
+ # == Inputs
10
+ # size:: [Integer] number of datapoints window should be
11
+ # window:: [Digiproc::WindowStrategy] desired window strategy
12
+ # wo:: [Float] center frequency in radians
13
+ # bw:: [Float] bandwidth in radians
14
+ # wlp_upper:: [Float] Upper frequency limit (radians) of the lowpass passband
15
+ # whp_lower:: [Float] Lower frequency limit (radians) of the highpass passband
16
+ # correct:: [Boolean] perform frequency corrections to make frequency points more accurate. Defaults to true
17
+ #
18
+ # Must have either `wo` and `bw` or `wlp_upper` and `whp_lower`
19
+ # For wo and bw, include the "don't care" areas in the bandstop area
20
+ #
21
+ ## Digiproc::BandpassFilter.new(size: 1000, wo: Math::PI / 4, bw: Math::PI / 10)
22
+ def initialize(size:, window: RectangularWindow, wo: nil, bw: nil, wlp_upper: nil , whp_lower: nil, correct: true )
23
+
24
+ super(size: size, window: window)
25
+
26
+ if !!wo && !!bw
27
+ bw += (@window.transition_width * 2 * PI)
28
+ wlp_upper = wo - bw / 2.0
29
+ whp_lower = wo + bw / 2.0
30
+ else
31
+ raise ArgumentError.new("You must provide either bandwidth and center freq or frequency bands") if wlp_upper.nil? or whp_lower.nil?
32
+ wlp_upper += @window.transition_width * PI if correct
33
+ whp_lower -= @window.transition_width * PI if correct
34
+ end
35
+
36
+ @equation = ->(n){
37
+ n == 0 ? (wlp_upper / PI) + (( PI - whp_lower )/ PI ) : ((Math.sin(wlp_upper * n) - Math.sin(whp_lower * n)) / (PI * n))
38
+ }
39
+ ideal_filter = calculate_ideal
40
+ @weights = self.window.values.times ideal_filter
41
+ @fft = FFT.new(data: self.weights)
42
+ @fft.calculate
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ ##
2
+ # Parent class to BandpassFilter, HighpassFilter, LowpassFilter, and BandstopFilter
3
+ class Digiproc::DigitalFilter
4
+ PI = Math::PI
5
+
6
+ attr_accessor :size, :window, :fft, :weights
7
+
8
+ ##
9
+ # == Inputs
10
+ # size:: [Integer] number of window datapoints
11
+ # window:: [Digiproc::WindowStrategy]
12
+ def initialize(size: , window: )
13
+ #TODO: allow size to be even
14
+ @size = size.even? ? size + 1 : size
15
+ @window = window.new(size: size)
16
+ end
17
+
18
+ ##
19
+ # Ensures size is odd, and uses @equation to make a return Array of ideal filter values.
20
+ # Used by the child class to multiply by the window to the return value of this method for final weights
21
+ def calculate_ideal
22
+ #TODO: allow size to be even
23
+ @size += 1 if @size.even?
24
+ n_vals = ((-1 * (@size - 1) / 2)..((@size - 1) / 2)).to_a
25
+ n_vals.map do |n|
26
+ @equation.call(n)
27
+ end
28
+ end
29
+
30
+
31
+ ##
32
+ # Zero pad @weights to achieve a size of the input value.
33
+ # set @fft to a new Digiproc::FFT, and calculate with the new padded data.
34
+ ## .set_fft_size(size [Integer])
35
+ def set_fft_size(size)
36
+ if size > @weights.length
37
+ zeros = Array.new(size - @weights.length, 0)
38
+ padded = @weights.concat(zeros)
39
+ @fft = Digiproc::FFT.new(data: padded)
40
+ @fft.calculate
41
+ end
42
+ end
43
+
44
+ ##
45
+ # return a Digiproc::DigitalSignal whose values are the weights of the filter
46
+ def to_ds
47
+ Digiproc::DigitalSignal.new(data: self.weights)
48
+ end
49
+
50
+ #TODO: Inorder to implement, must separately recalculate for weight at n = 0
51
+ # def shift_in_freq(normalized_freq)
52
+ # eqn = ->(n){ Math::E ** Complex(0, -1 * normalized_freq * n)}
53
+ # @weights = @weights.map.with_index do |w, i|
54
+ # w * eqn.call(i)
55
+ # end
56
+ # end
57
+
58
+
59
+ end
@@ -0,0 +1,27 @@
1
+ ##
2
+ # Creates a highpass filter via the windowing method
3
+ class Digiproc::HighpassFilter < Digiproc::DigitalFilter
4
+ attr_accessor :equation
5
+
6
+ ##
7
+ # == Inputs
8
+ # size:: [Integer] number of datapoints window should be
9
+ # window:: [Digiproc::WindowStrategy] desired window strategy
10
+ # wo:: [Float] center frequency in radians
11
+ # bw:: [Float] bandwidth in radians
12
+ # correct:: [Boolean] perform frequency corrections to make frequency points more accurate. Defaults to true
13
+ #
14
+ ## Digiproc::BandpassFilter.new(size: 1000, wo: Math::PI / 4, bw: Math::PI / 10)
15
+ def initialize(size:, window: RectangularWindow, wc: , correct: true)
16
+ super(size: size, window: window)
17
+ wc = wc - @window.transition_width * PI if correct
18
+ @equation = ->(n){
19
+ n == 0 ? (( PI - wc) / PI) : (-1 * (Math.sin( wc * n) / (PI * n)))
20
+ }
21
+ ideal_filter = calculate_ideal
22
+ @weights = self.window.values.times ideal_filter
23
+ @fft = FFT.new(data: self.weights)
24
+ @fft.calculate
25
+ end
26
+
27
+ end
@@ -0,0 +1,27 @@
1
+ ##
2
+ # Creates a Lowpass filter via the windowing method
3
+ class Digiproc::LowpassFilter < Digiproc::DigitalFilter
4
+
5
+ attr_accessor :equation
6
+
7
+ ##
8
+ # == Inputs
9
+ # size:: [Integer] number of datapoints window should be
10
+ # window:: [Digiproc::WindowStrategy] desired window strategy
11
+ # wo:: [Float] center frequency in radians
12
+ # bw:: [Float] bandwidth in radians
13
+ # correct:: [Boolean] perform frequency corrections to make frequency points more accurate. Defaults to true
14
+ #
15
+ ## Digiproc::BandpassFilter.new(size: 1000, wo: Math::PI / 4, bw: Math::PI / 10)
16
+ def initialize(size:, window: RectangularWindow, wc: , correct: true)
17
+ super(size: size, window: window)
18
+ wc = wc + @window.transition_width * PI if correct
19
+ @equation = ->(n){
20
+ n == 0 ? (wc / PI) : (Math.sin(wc * n) / (PI * n))
21
+ }
22
+ ideal_filter = calculate_ideal
23
+ @weights = self.window.values.times ideal_filter
24
+ @fft = Digiproc::FFT.new(time_data: self.weights)
25
+ @fft.calculate
26
+ end
27
+ end
data/lib/functions.rb ADDED
@@ -0,0 +1,221 @@
1
+ ##
2
+ # Contains many class methods which perform useful functions
3
+ module Digiproc::Functions
4
+
5
+ extend Digiproc::Convolvable::ClassMethods, Digiproc::FourierTransformable::GenericMethods, Digiproc::DataProperties
6
+
7
+ ##
8
+ # Performs cross correlation cacluatlino for two arrays of numerics
9
+ def self.cross_correlation(data1, data2)
10
+ self.conv(data1, data2.reverse)
11
+ end
12
+
13
+ ##
14
+ # Maps an array of numerics to a range defined by a min and max
15
+ # Output an array of samples mapped to within the bounds of the min and max
16
+ # == Input args
17
+ # samples:: Array of Numerics
18
+ # min:: Float, the minimum desired value of the new mapped data
19
+ # max:: Float, the maximum desired value of the new mapped data
20
+ def self.map_data_to_range(samples, min, max)
21
+ target_center = (max + min) / 2.0
22
+ target_range = max.to_f - min
23
+ smax, smin = samples.max, samples.min
24
+ sample_center = (smax + smin) / 2.0
25
+ sample_range = smax.to_f - smin
26
+ center_map = target_center - sample_center
27
+ range_map = target_range / sample_range
28
+ mapping = ->(n){ (n + center_map) * range_map }
29
+ process(samples, mapping)
30
+ end
31
+
32
+ ##
33
+ # Return a lambda equation which will map a dataset within a starting min and max to a
34
+ # new dataset with a different min and max.
35
+ # == Input Args
36
+ # starting_min:: Float, minimum val of data to be mapped
37
+ # starting_max:: Float, maximum val of data to be mapped
38
+ # target_min:: Float, minimum value of data after mapping
39
+ # target_max:: Float, maximum value of data after mapping
40
+ def self.map_to_eqn(starting_min, starting_max, target_min, target_max)
41
+ target_center = center_of(target_max, target_min)
42
+ target_range = range_of(target_max, target_min)
43
+ starting_center = center_of(starting_max, starting_min)
44
+ starting_range = range_of(starting_max.to_f, starting_min)
45
+ range_map = target_range.to_f / starting_range
46
+ ->(n){ (n - starting_center) * range_map + target_center}
47
+ end
48
+
49
+ ##
50
+ # == Input args
51
+ # *args:: Can be a variable number of Floats, an array, or a hash with key value pairs of point locations with the value of probability values
52
+ # If a variable number of floats are an array of floats are given, they will be treated as points on a 1-dimentional line and the return will
53
+ # be an array of numbers which are the sampe points centered around the origin .
54
+ # If a hash is given, the keys will be assumed to be places on the 1D line and the values will be the corresponding probabilities of those points.
55
+ # The same calculation will be carried out, except taking inot account the appropriate weighting.
56
+ # Note: Arguents are X-dimensional ponins as array => optional hash value for probability
57
+ def self.translate_center_to_origin(*args)
58
+ args = args[0] if args.length == 1 and args.first.is_a? Enumerable
59
+ points = []
60
+ probabilities = []
61
+ dimensions = 0
62
+ if args.is_a? Hash
63
+ args.each do |k,v|
64
+ points << k
65
+ probabilities << v
66
+ end
67
+ else
68
+ points = args
69
+ probabilities = points.map{ 1.0 / points.length }
70
+ end
71
+
72
+ if probabilities.include?(nil)
73
+ probabilities.map{ 1.0 / points.length }
74
+ end
75
+ dimensions = points.first.length
76
+ weighted_points = points.map.with_index do |point, index|
77
+ point.map{|dimension| -1 * dimension * probabilities[index]}
78
+ end
79
+ a = weighted_points.reduce(Array.new(dimensions,0), :plus)
80
+ points.map{ |vector| vector.plus(a) }
81
+ end
82
+
83
+ ##
84
+ # Return the center of a max and min point
85
+ # == Input args
86
+ # max:: float
87
+ # min:: float
88
+ def self.center_of(max, min)
89
+ (max + min) / 2.0
90
+ end
91
+
92
+ ##
93
+ # Return the range of a max and min point
94
+ # == Input args
95
+ # max:: float
96
+ # min:: float
97
+ def self.range_of(max, min)
98
+ max.to_f - min
99
+ end
100
+
101
+ ##
102
+ # Return an arary of zeros of length num
103
+ # == Input args
104
+ # num:: Integer
105
+ def self.zeros(num)
106
+ Array.new(num, 0)
107
+ end
108
+
109
+ ##
110
+ # Return an array of ones of length numn
111
+ # == Input args
112
+ # num:: Integer
113
+ def self.ones(num)
114
+ Array.new(num, 1)
115
+ end
116
+
117
+ ##
118
+ # Similar to linspace in numpy, return an array of numbers given a min, max, and total number
119
+ # of elements
120
+ # == Input args
121
+ # start:: flaot, the minimum number and index 0 of the output array
122
+ # stop:: float, the maximum number and last index of the output array
123
+ # number:: integer, the number of elements in the output array
124
+ def self.linspace(start, stop, number)
125
+ rng = 0...number
126
+ interval = (stop - start).to_f / (number - 1)
127
+ rng.map{ |val| start + interval * val }
128
+ end
129
+
130
+ ##
131
+ # Given an array of numerics, if it is monitonic (ie flat, increaseing, or decreasing), return
132
+ # a symbol representing its state. Return nil of the data is not monotonic
133
+ # == Input args
134
+ # data:: Array[Numeric]
135
+ def self.monotonic_state(data)
136
+ last_value = data.first
137
+ state = [:flat, :increasing, :decreasing]
138
+ state_index = 0
139
+ for i in 1...data.length do
140
+ return state[state_index] if state[state_index].nil?
141
+ slope = data[i] - last_value
142
+ if slope > 0
143
+ state_index = 1 if state[:state_index] == :flat
144
+ return nil if state[:state_index] != :increasing
145
+ elsif slope < 0
146
+ state_index = 2 if state[:state_index] == :flat
147
+ return nil if state[:state_index] != :decreasing
148
+ end
149
+ last_value = data[i]
150
+ end
151
+ return state[:state_index]
152
+ end
153
+
154
+ ##
155
+ # Return true or false based on if the data is monotoic (ie has a consistant slope direction)
156
+ # ==Input Arg
157
+ # data:: Array[Numeric]
158
+ def self.monotonic?(data)
159
+ !!monotonic_state(data)
160
+ end
161
+
162
+ ##
163
+ # Transform a binary number represented in string form into an integer
164
+ # == Input Arg
165
+ # bin_str:: String
166
+ ## Digiproc::Functions.bin_str_to_i("101101") # => 45
167
+ def self.bin_str_to_i(bin_str)
168
+ str_arr = bin_str.split("").reverse
169
+ sum = 0
170
+ str_arr.each_with_index do |digit, index|
171
+ sum += digit.to_i * 2 ** index
172
+ end
173
+ sum
174
+ end
175
+
176
+ ##
177
+ # XOR two strings representing binary numbers, return the XOR of them in decimal
178
+ ## Digiproc::Functions.str_xor("1011","1001") # => 2
179
+ def self.str_xor(str1, str2)
180
+ bin_str_to_i(str1) ^ bin_str_to_i(str2)
181
+ end
182
+
183
+
184
+
185
+ @fact_memo = {}
186
+
187
+ ##
188
+ # Given a number, return its factorial
189
+ def self.fact(n)
190
+ raise ArgumentError.new("n must be positive") if n < 0
191
+ return 1 if n <= 1
192
+ return @fact_memo[n] if not @fact_memo[n].nil?
193
+ x = n * fact(n - 1)
194
+ @fact_memo[n] = x
195
+ return x
196
+ end
197
+
198
+ # def self.populate_large_factorial_memoization
199
+ # for i in 1..10 do
200
+ # fact(10000 * i)
201
+ # end
202
+ # end
203
+
204
+ ##
205
+ # Return the value of sinc(x), given an input x (float)
206
+ # sinc = sin(x) / x
207
+ def self.sinc(x)
208
+ return 1 if x == 0
209
+ Math.sin(x) / x.to_f
210
+ end
211
+
212
+ ##
213
+ # Maps values using the lambda equation
214
+ # == Input args
215
+ # values:: Collection of values
216
+ # eqn:: lambda
217
+ def self.process(values, eqn)
218
+ values.map{ |val| eqn.call(val) }
219
+ end
220
+
221
+ end