digiproc 0.1.0

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 (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