digiproc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/Rakefile +37 -0
- data/TODO.md +50 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/environment.rb +118 -0
- data/console_tests.rb +44 -0
- data/digiproc.gemspec +49 -0
- data/examples/analog_signals/analog_to_digital.rb +31 -0
- data/examples/analog_signals/companded-signals.png +0 -0
- data/examples/analog_signals/companding.rb +68 -0
- data/examples/analog_signals/fft-plot.png +0 -0
- data/examples/analog_signals/plot_Digiproc::FFT.png +0 -0
- data/examples/analog_signals/plot_Dsp::FFT.png +0 -0
- data/examples/analog_signals/quantization-outputs.png +0 -0
- data/examples/analog_signals/quantize_compand.rb +69 -0
- data/examples/binomial_distribution/bit_error.rb +14 -0
- data/examples/binomial_distribution/dice.rb +35 -0
- data/examples/digital_signals/_coded_frequency_signal,_ts_=_1_s.png +0 -0
- data/examples/digital_signals/_coded_frequency_signal,_ts_=_2_s.png +0 -0
- data/examples/digital_signals/coded_power_spectral_density,__ts_=_1_s.png +0 -0
- data/examples/digital_signals/coded_power_spectral_density,__ts_=_2_s.png +0 -0
- data/examples/digital_signals/coded_time_signal,_ts_=_1_s.png +0 -0
- data/examples/digital_signals/coded_time_signal,_ts_=_2_s.png +0 -0
- data/examples/digital_signals/freq_sig_from_eqn,_ts_=_1_s.png +0 -0
- data/examples/digital_signals/freq_sig_from_eqn,_ts_=_2_s.png +0 -0
- data/examples/digital_signals/frequency_signal,_ts_=_1_s.png +0 -0
- data/examples/digital_signals/frequency_signal,_ts_=_2_s.png +0 -0
- data/examples/digital_signals/modulate_square_pulses.rb +9 -0
- data/examples/digital_signals/modulated_sq._pulses.png +0 -0
- data/examples/digital_signals/modulated_sq._pulses_alt.png +0 -0
- data/examples/digital_signals/power_spectral_density,__ts_=_1_s.png +0 -0
- data/examples/digital_signals/power_spectral_density,__ts_=_2_s.png +0 -0
- data/examples/digital_signals/square_signals.rb +90 -0
- data/examples/digital_signals/time_signal,_ts_=_1_s.png +0 -0
- data/examples/digital_signals/time_signal,_ts_=_2_s.png +0 -0
- data/examples/encoding/gray_code.rb +22 -0
- data/examples/encoding/psk.rb +91 -0
- data/examples/encoding/system_2_phase.png +0 -0
- data/examples/encoding/system_2_xmit_signal.png +0 -0
- data/examples/encoding/system_3_phase.png +0 -0
- data/examples/encoding/system_3_xmit_signal.png +0 -0
- data/examples/encoding/system_4_xmit_signal.png +0 -0
- data/examples/encoding/xor-dpsk-phase-signal-(sys1).png +0 -0
- data/examples/encoding/xor-dpsk-xmit-signal-(sys-1).png +0 -0
- data/examples/factories/Quickplot Graph.png +0 -0
- data/examples/factories/bandpass.rb +6 -0
- data/examples/fft/plot_Dsp::FFT.png +0 -0
- data/examples/fft/recieved_data_(time_domain).png +0 -0
- data/examples/fft/simple_fft_example.rb +47 -0
- data/examples/fft/unprocessed_fft.png +0 -0
- data/examples/filters/bandpass_filter.png +0 -0
- data/examples/filters/filter_a_signal.rb +38 -0
- data/examples/filters/white_noise_db_out_of_bp_filter.png +0 -0
- data/examples/filters/white_noise_mag_out_of_bp_filter.png +0 -0
- data/examples/filters/white_noise_spectra.png +0 -0
- data/examples/functions/compute_probability.rb +29 -0
- data/examples/functions/gram_schmidt.rb +10 -0
- data/examples/functions/minimize_energy.rb +29 -0
- data/examples/functions/orthoganalize.rb +18 -0
- data/examples/functions/simple_functions.rb +81 -0
- data/examples/linear_algebra/diverging_sys.rb +13 -0
- data/examples/linear_algebra/iterative_sys_of_eqns_methods.rb +27 -0
- data/examples/modulation_schemes/dpsk_2.png +0 -0
- data/examples/modulation_schemes/dpsk_256.png +0 -0
- data/examples/modulation_schemes/dpsk_freq_domain.rb +119 -0
- data/examples/modulation_schemes/psk.rb +36 -0
- data/examples/modulation_schemes/psk_2.png +0 -0
- data/examples/modulation_schemes/psk_256.png +0 -0
- data/examples/modulation_schemes/psksystem_1_xmit_signal.png +0 -0
- data/examples/modulation_schemes/psksystem_2_xmit_signal.png +0 -0
- data/examples/modulation_schemes/psksystem_3_xmit_signal.png +0 -0
- data/examples/modulation_schemes/system_1_xmit_signal.png +0 -0
- data/examples/modulation_schemes/system_2_xmit_signal.png +0 -0
- data/examples/modulation_schemes/system_3_xmit_signal.png +0 -0
- data/examples/quickplot/PlottableClass_plot.png +0 -0
- data/examples/quickplot/decorators.rb +13 -0
- data/examples/quickplot/direct_gruff.png +0 -0
- data/examples/quickplot/plot_PlottableClass.png +0 -0
- data/examples/quickplot/quickplot_vs_others.rb +85 -0
- data/examples/quickplot/random_data_quickplot,_dark.png +0 -0
- data/examples/quickplot/random_data_quickplot.png +0 -0
- data/examples/realized_gaussian/norm_dist_plot.png +0 -0
- data/examples/realized_gaussian/norm_dist_spectrum.png +0 -0
- data/examples/realized_gaussian/realized_gaussian_example.rb +23 -0
- data/lib/concerns/convolvable.rb +144 -0
- data/lib/concerns/data_properties.rb +223 -0
- data/lib/concerns/fourier_transformable.rb +178 -0
- data/lib/concerns/initializable.rb +43 -0
- data/lib/concerns/multipliable.rb +22 -0
- data/lib/concerns/os.rb +36 -0
- data/lib/concerns/plottable.rb +248 -0
- data/lib/concerns/requires_data.rb +8 -0
- data/lib/digiproc/version.rb +8 -0
- data/lib/digiproc.rb +2 -0
- data/lib/extensions/array_extension.rb +23 -0
- data/lib/extensions/core_extensions.rb +117 -0
- data/lib/factories/factories.rb +3 -0
- data/lib/factories/filter_factory.rb +83 -0
- data/lib/factories/window_factory.rb +22 -0
- data/lib/fft.rb +255 -0
- data/lib/filters/bandpass_filter.rb +43 -0
- data/lib/filters/bandstop_filter.rb +44 -0
- data/lib/filters/digital_filter.rb +59 -0
- data/lib/filters/highpass_filter.rb +27 -0
- data/lib/filters/lowpass_filter.rb +27 -0
- data/lib/functions.rb +221 -0
- data/lib/probability/binomial_distribution.rb +84 -0
- data/lib/probability/bit_generator.rb +94 -0
- data/lib/probability/gaussian_distribution.rb +29 -0
- data/lib/probability/probability.rb +234 -0
- data/lib/probability/theoretical_gaussian_distribution.rb +59 -0
- data/lib/quick_plot.rb +96 -0
- data/lib/rbplot.rb +219 -0
- data/lib/signals/analog_signal.rb +143 -0
- data/lib/signals/digital_signal.rb +181 -0
- data/lib/strategies/code/differential_encoding_strategy.rb +69 -0
- data/lib/strategies/code/gray_code.rb +75 -0
- data/lib/strategies/code/xor_differential_encoding_strategy.rb +100 -0
- data/lib/strategies/code/xor_differential_encoding_zero_angle_strategy.rb +103 -0
- data/lib/strategies/companding/custom_companding_strategy.rb +29 -0
- data/lib/strategies/convolution/bf_conv.rb +57 -0
- data/lib/strategies/fft/brute_force_dft_strategy.rb +31 -0
- data/lib/strategies/fft/inverse_fft_conjugate_strategy.rb +44 -0
- data/lib/strategies/fft/radix2_strategy.rb +84 -0
- data/lib/strategies/gaussian/gaussian_generator.rb +49 -0
- data/lib/strategies/linear_algebra/gauss_seidel_strategy.rb +90 -0
- data/lib/strategies/linear_algebra/jacobi_strategy.rb +81 -0
- data/lib/strategies/linear_algebra/sor2_strategy.rb +98 -0
- data/lib/strategies/linear_algebra/sor_strategy.rb +108 -0
- data/lib/strategies/modulation/phase_shift_keying_strategy.rb +96 -0
- data/lib/strategies/orthogonalize/gram_schmidt.rb +50 -0
- data/lib/strategies/strategies.rb +3 -0
- data/lib/strategies/window/blackman_window.rb +32 -0
- data/lib/strategies/window/hamming_window.rb +31 -0
- data/lib/strategies/window/hanning_window.rb +31 -0
- data/lib/strategies/window/kaiser_window.rb +27 -0
- data/lib/strategies/window/rectangular_window.rb +22 -0
- data/lib/strategies/window/window.rb +42 -0
- data/lib/systems/custom_system.rb +13 -0
- data/lib/systems/hilbert_transform.rb +6 -0
- data/lib/systems/matched_filter.rb +21 -0
- data/lib/systems/raised_cosine_filter.rb +11 -0
- data/lib/systems/system.rb +19 -0
- data/lib/systems/systems.rb +3 -0
- data/playground.rb +323 -0
- data/plots/_coded_frequency_signal,_ts_=_1_s.png +0 -0
- data/plots/_coded_frequency_signal,_ts_=_2_s.png +0 -0
- data/plots/coded_freq_sig_from_eqn,_ts_=_1_s.png +0 -0
- data/plots/coded_freq_sig_from_eqn,_ts_=_2_s.png +0 -0
- data/plots/coded_power_spectral_density,__ts_=_1_s.png +0 -0
- data/plots/coded_power_spectral_density,__ts_=_2_s.png +0 -0
- data/plots/coded_time_signal,_ts_=_1_s.png +0 -0
- data/plots/coded_time_signal,_ts_=_2_s.png +0 -0
- data/plots/dpsk_2.png +0 -0
- data/plots/freq_sig_from_eqn,_ts_=_1_s.png +0 -0
- data/plots/freq_sig_from_eqn,_ts_=_2_s.png +0 -0
- data/plots/frequency_signal,_ts_=_1_s.png +0 -0
- data/plots/frequency_signal,_ts_=_2_s.png +0 -0
- data/plots/power_spectral_density,__ts_=_1_s.png +0 -0
- data/plots/power_spectral_density,__ts_=_2_s.png +0 -0
- data/plots/psk_2.png +0 -0
- data/plots/time_signal,_ts_=_1_s.png +0 -0
- data/plots/time_signal,_ts_=_2_s.png +0 -0
- data/test-title-dark.png +0 -0
- data/test-title.png +0 -0
- 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
|