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