Acoular 25.01 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 blockwise FFT and 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
num_samples = int(sample_freq * t_in_s)

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

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

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

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

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
/home/runner/work/acoular/acoular/examples/io_and_signal_processing_examples/example_fft.py:111: DeprecationWarning: Deprecated use of 'naverage' trait. Please use the 'num_per_average' trait instead.
  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
/home/runner/work/acoular/acoular/examples/io_and_signal_processing_examples/example_fft.py:120: DeprecationWarning: Deprecated use of 'naverage' trait. Please use the 'num_per_average' trait instead.
  avg.naverage = navg

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, num_channels, num_channels)
csm = csm.reshape(fft.numfreqs, ts.num_channels, ts.num_channels)
/home/runner/work/acoular/acoular/examples/io_and_signal_processing_examples/example_fft.py:150: DeprecationWarning: Deprecated use of 'naverage' trait. Please use the 'num_per_average' trait instead.
  avg = ac.Average(source=ps, naverage=int(ps1.num_blocks))
/home/runner/work/acoular/acoular/examples/io_and_signal_processing_examples/example_fft.py:154: DeprecationWarning: Deprecated use of 'numfreqs' trait. Please use the 'num_freqs' trait instead.
  csm = csm.reshape(fft.numfreqs, ts.num_channels, ts.num_channels)

Plot both spectra

plt.figure()
colors = ['r', 'b']
for i in range(ts.num_channels):
    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.164 seconds)

Gallery generated by Sphinx-Gallery

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