Acoular 24.10 documentation

Fast Fourier Transform (FFT) of multichannel time data.

«  Parallel processing chains – SampleSplitter buffer handling.   ::   IO and signal processing examples   ::   Tools  »

Fast Fourier Transform (FFT) of multichannel time data.

Demonstrates how to calculate the FFT of a signal blockwise and how to create a spectrogram of the signal.

Imports

import acoular as ac
import numpy as np

We define two sine wave signals with different frequencies (1000 Hz and 4000 Hz) and a noise signal. Then, the signals are calculated and added together.

sample_freq = 25600
t_in_s = 30.0
numsamples = int(sample_freq * t_in_s)

sine1 = ac.SineGenerator(sample_freq=sample_freq, numsamples=numsamples, freq=1000, amplitude=2.0)
sine2 = ac.SineGenerator(sample_freq=sample_freq, numsamples=numsamples, freq=4000, amplitude=0.5)
noise = ac.WNoiseGenerator(sample_freq=sample_freq, numsamples=numsamples, rms=0.5)
mixed_signal = (sine1.signal() + sine2.signal() + noise.signal())[:, np.newaxis]

The mixed signal is then used to create a TimeSamples object.

ts = ac.TimeSamples(data=mixed_signal, sample_freq=sample_freq)
print(ts.numsamples, ts.numchannels)
768000 1

Create a spectrogram of the signal

Therefore we want to process the FFT spectra of the signal blockwise. To do so, we use the acoular.fprocess.RFFT class, which calculates the FFT spectra of the signal blockwise.

block_size = 512

fft = ac.RFFT(source=ts, block_size=block_size, window='Rectangular')  # results in the amplitude spectra

spec = next(fft.result(num=1))[0]  # return a single snapshot
time_block = next(ts.result(num=block_size))[:, 0]

print("Parseval's theorem:")
print('signal energy in time domain', np.round(np.sum(time_block**2)))
print(
    'signal energy in frequency domain (single-sided)',
    np.round((1 / block_size * np.sum(spec * spec.conjugate())).real),
)

# It can be seen, that the energy is not preserved when using the default 'none' scaling.
# Therefore, we set the scaling to 'energy' to compensate for the energy loss due to truncation to a single-sided spectrum.

fft.scaling = 'energy'
spec = next(fft.result(num=1))[0]
print(
    'signal energy in frequency domain (single-sided) with energy scaling',
    np.round((1 / block_size * np.sum(spec * spec.conjugate() / 2)).real),
)

fft.scaling = 'none'
ifft = ac.IRFFT(source=fft)
time_block_rec = next(ifft.result(num=block_size))[:, 0]
print('reconstructed signal energy in time domain', np.round(np.sum(time_block_rec**2)))
Parseval's theorem:
signal energy in time domain 1231.0
signal energy in frequency domain (single-sided) 616.0
signal energy in frequency domain (single-sided) with energy scaling 1231.0
reconstructed signal energy in time domain 1231.0

Plot the spectrogram (time-frequency representation) of the signal. Here, we plot the amplitude spectra for each time block. Therefore, we normalize the fft spectra by the number of samples in the block by setting the scaling to 'amplitude'. It can be varified from the spectrogram plot that the amplitude of the tones are correctly represented.

import matplotlib.pyplot as plt

fft.scaling = 'amplitude'
spectrogram = ac.tools.return_result(fft)

# plot the power spectrogram
plt.figure()
plt.imshow(np.abs(spectrogram.T), origin='lower', aspect='auto', extent=(0, t_in_s, 0, sample_freq / 2), vmax=2.0)
plt.xlabel('Time / s')
plt.ylabel('Frequency / Hz')
plt.colorbar(label='Power Spectrum')
plt.show()

plt.figure()
for block_size in [4096, 256]:
    fft.block_size = block_size
    spec = next(fft.result(num=1))[0]
    plt.plot(fft.freqs, np.abs(spec), label=f'block_size = {block_size}')
plt.xlim(0, sample_freq / 2)
plt.xlabel('Frequency / Hz')
plt.ylabel('Amplitude Spectrum')
plt.legend()
plt.grid()
plt.show()
  • example fft
  • example fft

Create an averaged power spectral density of the signal

To calculate the time averaged power spectral density of the signal, we use the acoular.fprocess.AutoPowerSpectra class.

fft.scaling = 'none'
ap = ac.AutoPowerSpectra(source=fft, scaling='psd')  # results in the power spectrum
avg = ac.Average(source=ap, naverage=64)  # results in the time averaged power spectrum

Plot the resulting power spectrum for different number of averages.

for block_size in [256, 1024]:
    plt.figure()
    fft.block_size = block_size
    for navg in [1, 100]:
        avg.naverage = navg
        spectrum = next(avg.result(num=1))
        levels = ac.L_p(spectrum)
        plt.plot(fft.freqs, levels[0], label=f'navg = {navg}, block_size = {block_size}')
    plt.xlabel('Frequency / Hz')
    plt.ylabel('PSD / dB')
    plt.grid()
    plt.legend()
    plt.semilogx()
    plt.show()
  • example fft
  • example fft

Create a cross-spectral matrix for multichannel signals

To calculate the cross-spectral matrix of the signal, we use the acoular.fprocess.CrossPowerSpectra class. First, we create a TimeSamples object with two channels. Then, we calculate the cross-spectral matrix of the signal blockwise. We choose a normalization method of ‘psd’ (Power Spectral Density) for the cross-spectral matrix.

fft.block_size = 128

mixed_signal = noise.signal()[:, np.newaxis]
ts = ac.TimeSamples(data=np.concatenate([mixed_signal, 0.5 * mixed_signal], axis=1), sample_freq=sample_freq)

# compare with PowerSpectra
ps1 = ac.PowerSpectra(source=ts, block_size=fft.block_size, cached=False)
csm_comp = ps1.csm[:, :, :]

fft.source = ts  # we need to change the source of the fft object to the new TimeSamples object
ps = ac.CrossPowerSpectra(source=fft)
avg = ac.Average(source=ps, naverage=int(ps1.num_blocks))
csm = next(avg.result(num=1))

# reshape the cross-spectral matrix to a 3D array of shape (numfreq, numchannels, numchannels)
csm = csm.reshape(fft.numfreqs, ts.numchannels, ts.numchannels)

Plot both spectra

plt.figure()
colors = ['r', 'b']
for i in range(ts.numchannels):
    c = colors[i]
    auto_pow_spectrum = ac.L_p(csm_comp[:, i, i].real)
    plt.plot(fft.freqs, auto_pow_spectrum, label=f'PowerSpectra ({i})', color=c, linestyle='-', linewidth=0.8)
    auto_pow_spectrum = ac.L_p(csm[:, i, i].real)
    plt.plot(fft.freqs, auto_pow_spectrum, label=f'CrossPowerSpectra ({i})', color=c, linestyle='--')
plt.xlabel('Frequency / Hz')
plt.ylabel('Power Spectrum / dB')
plt.grid()
plt.legend()
plt.semilogx()
plt.show()
example fft

Total running time of the script: (0 minutes 2.497 seconds)

Gallery generated by Sphinx-Gallery

«  Parallel processing chains – SampleSplitter buffer handling.   ::   IO and signal processing examples   ::   Tools  »