Class: Digiproc::FFT

Inherits:
Object
  • Object
show all
Includes:
Convolvable::InstanceMethods, Plottable::InstanceMethods
Defined in:
lib/fft.rb

Overview

Class to calculate and store the Discrete Fourier Transform of a siugnal

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Convolvable::InstanceMethods

#acorr, #auto_correlation, #conv, #convolution_strategy, #convolve, #cross_correlation, included, #xcorr

Methods included from Plottable::InstanceMethods

#plot, #qplot

Constructor Details

#initialize(strategy: Digiproc::Strategies::Radix2Strategy, time_data: nil, size: nil, window: Digiproc::RectangularWindow, freq_data: nil, inverse_strategy: Digiproc::Strategies::IFFTConjugateStrategy) ⇒ FFT

Input Args

strategy

FFT Strategy, see Digiproc::Strategies::Radix2Strategy to see required Protocol

time_data

Array time data to be transformed to the frequency domain via the FFT strategy

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

window (Optional)

Digiproc::Window, defaults to Digiproc::RectangularWindow. Changes the window used during #process_with_window method

freq_data (Optional)

Array, required if time_data not given

inverse_strategy (Optional)

Digiproc::Strategies::IFFTConjugateStrategy is the default value and shows the required protocol

Note: Using size with a Radix2Strategy will only ensure a minimum amount of zero-padding, it will mostly likely not determine the final size of the time_data You need to have EITHER time_data or freq_data, but not both.

Raises:

  • (ArgumentError)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/fft.rb', line 46

def initialize(strategy: Digiproc::Strategies::Radix2Strategy, time_data: nil, size: nil, window: Digiproc::RectangularWindow, freq_data: nil, inverse_strategy: Digiproc::Strategies::IFFTConjugateStrategy)
    raise ArgumentError.new("Either time or frequency data must be given") if time_data.nil? and freq_data.nil?
    raise ArgumentError.new('Size must be an integer') if not size.nil? and not size.is_a?(Integer) 
    raise ArguemntError.new('Size must be greater than zero') if not size.nil? and size <= 0 
    raise ArgumentError.new('time_data must be an array') if not time_data.respond_to?(:calculate) and not time_data.is_a? Array
    
    if time_data.is_a? Array
        @time_data_size = time_data.length
        if not size.nil?
            if size <= time_data.length
                @time_data = time_data.dup.map{ |val| val.dup }.take(size)
            else 
                zero_fill = Array.new(size - time_data.length, 0)
                @time_data = time_data.dup.map{ |val| val.dup }.concat zero_fill
            end
        else
            @time_data = time_data.dup.map{ |val| val.dup}
        end
        @strategy = strategy.new(@time_data.map{ |val| val.dup})
        @window = window.new(size: time_data_size)
    else
        @time_data = time_data
        @strategy = strategy.new
        @window = window.new(size: freq_data.length)
    end
    @inverse_strategy = inverse_strategy
    @data = freq_data
end

Instance Attribute Details

#inverse_strategyObject

Returns the value of attribute inverse_strategy.



31
32
33
# File 'lib/fft.rb', line 31

def inverse_strategy
  @inverse_strategy
end

#processed_time_dataObject

Returns the value of attribute processed_time_data.



31
32
33
# File 'lib/fft.rb', line 31

def processed_time_data
  @processed_time_data
end

#strategyObject

Returns the value of attribute strategy.



31
32
33
# File 'lib/fft.rb', line 31

def strategy
  @strategy
end

#time_data_sizeObject

Returns the value of attribute time_data_size.



31
32
33
# File 'lib/fft.rb', line 31

def time_data_size
  @time_data_size
end

#windowObject

Returns the value of attribute window.



31
32
33
# File 'lib/fft.rb', line 31

def window
  @window
end

Class Method Details

.calculate(time_data) ⇒ Object

Calculate the FFT of given time data

Input arg

time_data

Array



9
10
11
# File 'lib/fft.rb', line 9

def self.calculate(time_data)
    Radix2Strategy.calculate(time_data)
end

.new_from_spectrum(data) ⇒ Object

Calculate the IFFT of the given frequency data Input frequency data, perform the Inverse FFT to populate the time data

Input arg

data

Array



18
19
20
21
# File 'lib/fft.rb', line 18

def self.new_from_spectrum(data)
    time_data = Digiproc::Strategies::IFFTConjugateStrategy.new(data)
    new(freq_data: data, time_data: time_data)
end

Instance Method Details

#*(obj) ⇒ Object

Allows multioplication of FFT objects with anything with a @data reader which holds an Array of Numerics. The return value is a new FFT object whose frequency data is the element-by-element multiplication of the two data arrays



206
207
208
209
210
211
212
# File 'lib/fft.rb', line 206

def *(obj)
    if obj.respond_to?(:data) 
        return self.class.new_from_spectrum(self.data.times obj.data)
    elsif obj.is_a? Array 
        return self.class.new_from_spectrum(self.data.times obj)
    end
end

#angleObject

Returns the angle of the frequency domain data, as an array of floats (in radians)



167
168
169
# File 'lib/fft.rb', line 167

def angle
    self.data.map(&:angle)
end

#calculateObject

Performs FFT caclulation if not yet performed. Returns FFT data as an Array of Floats (or an array of Complex numbers)



77
78
79
80
81
# File 'lib/fft.rb', line 77

def calculate
    self.strategy.data = time_data if @strategy.data.nil?
    @fft = self.strategy.calculate
    @data = @fft
end

#calculate_at_size(size) ⇒ Object

Input argument of an Integer describing the required size of the FFT. IF using a strategy requiring a certain amount of 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
87
88
89
90
91
92
93
94
95
# File 'lib/fft.rb', line 86

def calculate_at_size(size)
    if size > self.data.size
        zero_fill = Array.new(size - @time_data.length, 0)
        @time_data = time_data.concat zero_fill
    elsif size < self.data.size
        @time_data = time_data.take(size)
    end
    self.strategy.data = time_data
    calculate
end

#conjugateObject

Return the complex conjugate of the frequency domain data, as an array of numerics (float or complex)



153
154
155
# File 'lib/fft.rb', line 153

def conjugate
    self.data.map(&:conjugate)
end

#dataObject

Reader for @data Allows for lazy calculation of @data (which holds frequency domain data) If @data is nil, the #calculate method will be called



26
27
28
29
# File 'lib/fft.rb', line 26

def data
    calculate if @data.nil?
    @data
end

#dBObject

Return the decible of the frequency domain data, as an Array of floats



159
160
161
162
163
# File 'lib/fft.rb', line 159

def dB
    self.magnitude.map do |m|
        Math.db(m)
    end
end

#fftObject

Returns the frequency domain data as an Array of Numerics (Float or Complex)



133
134
135
# File 'lib/fft.rb', line 133

def fft
    self.data
end

#graph_magnitude(file_name = "fft") ⇒ Object

TODO: Remove plots magnitude using Gruff directly



238
239
240
241
242
243
244
# File 'lib/fft.rb', line 238

def graph_magnitude(file_name = "fft")
    if @fft
        g = Gruff::Line.new
        g.data :fft, self.magnitude
        g.write("./#{file_name}.png")
    end
end

#graph_time_dataObject

TODO: Remove Plots time data using Gruff directly



249
250
251
252
# File 'lib/fft.rb', line 249

def graph_time_data
    g = Gruff::Line.new
    g.data :data, @time_data
end

#ifftObject

Calculate the IFFT of the frequency data



99
100
101
# File 'lib/fft.rb', line 99

def ifft
    inverse_strategy.new(data).calculate
end

#ifft_dsObject

Calculate the IFFT and return it as a Digiproc::DigitalSignal



105
106
107
# File 'lib/fft.rb', line 105

def ifft_ds
    Digiproc::DigitalSignal.new(data: ifft)
end

#imaginaryObject

Returns the imaginary part of the frequency domain data, as an array of floats



179
180
181
# File 'lib/fft.rb', line 179

def imaginary
    self.data.map(&:imaginary)
end

#local_maxima(num = 1) ⇒ Object

Returns the local maximum value(s) of the magnitude of the frequency signal as an Array of OpenStructs with an index and value property. Local maxima are determined by Digiproc::DataProperties.local_maxima, and the returned maxima are determined based off of their relative hight to adjacent maxima. This is useful for looking for spikes in frequency data

Input arg

num (Optional)

The number of maxima desired, defaults to 1



199
200
201
# File 'lib/fft.rb', line 199

def local_maxima(num = 1)
    Digiproc::DataProperties.local_maxima(self.magnitude, num)
end

#magnitudeObject

Return the magnitude of the frequency domain values as an array of floats



145
146
147
148
149
# File 'lib/fft.rb', line 145

def magnitude
    data.map do |f|
        f.abs
    end
end

#maxima(num = 1) ⇒ Object

Returns the maximum value(s) of the magnitude of the frequency signal as an Array of OpenStructs with an index and value property.

Input arg

num (Optional)

The number of maxima desired, defaults to 1



188
189
190
# File 'lib/fft.rb', line 188

def maxima(num = 1)
    Digiproc::DataProperties.maxima(self.magnitude, num)
end

#plot_db(path: "./") ⇒ Object

Uses Plottable module to plot the db values



216
217
218
219
220
221
222
# File 'lib/fft.rb', line 216

def plot_db(path: "./") 
    self.plot(method: :dB, xsteps: 8, path: path) do |g|
        g.title = "Decibles"
        g.x_axis_label = "Normalized Frequency"
        g.y_axis_label = "Magnitude"
    end
end

#plot_magnitude(path: "./") ⇒ Object

uses Plottable module to plot the magnitude values



226
227
228
229
230
231
232
# File 'lib/fft.rb', line 226

def plot_magnitude(path: "./" )
    self.plot(method: :magnitude, xsteps: 8, path: path) do |g|
        g.title = "Magnitude"
        g.x_axis_label = "Normalized Frequency"
        g.y_axis_label = "Magnitude"
    end
end

#process_with_windowObject

Processes the time_data with the chosen window valuesand calculates the FFT based of of the window-processed time domain signals



124
125
126
127
128
129
# File 'lib/fft.rb', line 124

def process_with_window
    @processed_time_data = time_data.take(time_data_size).times self.window.values
    self.strategy.data = @processed_time_data
    @fft = self.strategy.calculate
    @data = @fft
end

#realObject

Returns the real part of the frequency domain data, as an array of floats



173
174
175
# File 'lib/fft.rb', line 173

def real
    self.data.map(&:real)
end

#sizeObject

Return the number of frequency domain datapoints



139
140
141
# File 'lib/fft.rb', line 139

def size
    self.data.length
end

#time_dataObject

Returns the time_data as an Array of Numerics (floats or Complex numbers)



112
113
114
115
116
117
118
119
120
# File 'lib/fft.rb', line 112

def time_data
    if @time_data.is_a? Array
        @time_data
    elsif @time_data.respond_to? :calculate
        @time_data = @time_data.calculate
    else
        raise TypeError.new("time_data needs to be an array or an ifft strategy, not a #{@time_data.class}")
    end
end