Source code for pyfar.dsp.fft

"""
The following documents the FFT functionality. More details and background
including a complete list and explanation of available `FFT normalizations` is
given in the
:ref:`FFT notebook <gallery:/gallery/interactive/fast_fourier_transform.ipynb>`
of the pyfar gallery.
"""
import multiprocessing

import numpy as np
from scipy import fft as sfft


[docs] def rfftfreq(n_samples, sampling_rate): """ Returns the positive discrete frequencies for which the FFT is calculated. If the number of samples :math:`N` is even the number of frequency bins will be :math:`2/N+1`, if :math:`N` is odd, the number of bins will be :math:`(N+1)/2`. Parameters ---------- n_samples : int The number of samples in the signal sampling_rate : int The sampling rate of the signal Returns ------- frequencies : array, double The positive discrete frequencies in Hz for which the FFT is calculated. """ return sfft.rfftfreq(n_samples, d=1/sampling_rate)
[docs] def rfft(data, n_samples, sampling_rate, fft_norm): """ Calculate the FFT of a real-valued time-signal. The function returns only the right-hand side of the axis-symmetric spectrum. Details on the FFT normalization given by `fft_norm` are given above. Parameters ---------- data : array, double Array containing the time domain signal with dimensions (..., ``'n_samples'``) n_samples : int The number of samples sampling_rate : number sampling rate in Hz fft_norm : 'none', 'unitary', 'amplitude', 'rms', 'power', 'psd' See documentation of :py:func:`~pyfar.dsp.fft.normalization`. Returns ------- spec : array, complex The complex valued right-hand side of the spectrum with dimensions (..., n_bins) """ # DFT spec = sfft.rfft( data, n=n_samples, axis=-1, workers=multiprocessing.cpu_count()) # Normalization spec = normalization(spec, n_samples, sampling_rate, fft_norm, inverse=False, single_sided=True) return spec
[docs] def irfft(spec, n_samples, sampling_rate, fft_norm): """ Calculate the IFFT of a single-sided Fourier spectrum. The function takes only the right-hand side of the spectrum and returns a real-valued time signal. Details on the FFT normalization given by `fft_norm` are given found above. Parameters ---------- spec : array, complex The complex valued right-hand side of the spectrum with dimensions (..., n_bins) n_samples : int The number of samples of the corresponding time signal. This is crucial to allow for the correct transform of time signals with an odd number of samples. sampling_rate : number sampling rate in Hz fft_norm : 'none', 'unitary', 'amplitude', 'rms', 'power', 'psd' See :py:func:`~pyfar.dsp.fft.normalization`. Returns ------- data : array, double Array containing the time domain signal with dimensions (..., ``'n_samples'``) """ # Inverse normalization spec = normalization(spec, n_samples, sampling_rate, fft_norm, inverse=True, single_sided=True) # Inverse DFT data = sfft.irfft( spec, n=n_samples, axis=-1, workers=multiprocessing.cpu_count()) return data
[docs] def fftfreq(n_samples, sampling_rate): """ Returns the negative and positive discrete frequencies for which the FFT is calculated. The number of frequency bins equals n_samples. The zero frequency is shifted to the center. Parameters ---------- n_samples : int The number of samples in the signal sampling_rate : int The sampling rate of the signal Returns ------- frequencies : array, double The discrete frequencies in Hz for which the FFT is calculated. """ return sfft.fftshift(sfft.fftfreq(n_samples, d=1/sampling_rate))
[docs] def fft(data, n_samples, sampling_rate, fft_norm): """ Calculate the double-sided FFT of a time signal. The function returns the double sided spectrum. The normalization is considered according to ``'fft_norm'`` as described in :py:func:`~pyfar.dsp.fft.normalization` and :py:mod:`FFT concepts <pyfar._concepts.fft>`. Parameters ---------- data : array, double Array containing the time domain signal with dimensions (..., ``'n_samples'``) n_samples : int The number of samples sampling_rate : number sampling rate in Hz fft_norm : 'none', 'unitary', 'amplitude', 'rms', 'power', 'psd' See documentation of :py:func:`~pyfar.dsp.fft.normalization`. Returns ------- spec : array, complex The complex valued right-hand side of the spectrum with dimensions (..., n_bins) """ # DFT spec = sfft.fftshift(sfft.fft( data, n=n_samples, axis=-1, workers=multiprocessing.cpu_count()), axes=-1) # Normalization spec = normalization(spec, n_samples, sampling_rate, fft_norm, inverse=False, single_sided=False) return spec
[docs] def ifft(spec, n_samples, sampling_rate, fft_norm): """ Calculate the IFFT of a double-sided Fourier spectrum. The function takes double-sided spectrum and returns a time signal. The normalization is considered according to ``'fft_norm'`` as described in :py:func:`~pyfar.dsp.fft.normalization` and :py:mod:`FFT concepts <pyfar._concepts.fft>`. Parameters ---------- spec : array, complex The complex valued right-hand side of the spectrum with dimensions (..., n_bins) n_samples : int The number of samples of the corresponding time signal. This is crucial to allow for the correct transform of time signals with an odd number of samples. sampling_rate : number sampling rate in Hz fft_norm : 'none', 'unitary', 'amplitude', 'rms', 'power', 'psd' See :py:func:`~pyfar.dsp.fft.normalization`. Returns ------- data : array, double Array containing the time domain signal with dimensions (..., ``'n_samples'``) """ # Inverse normalization spec = normalization(spec, n_samples, sampling_rate, fft_norm, inverse=True, single_sided=False) # Inverse DFT return sfft.ifft( sfft.ifftshift(spec, axes=-1), n=n_samples, axis=-1, workers=multiprocessing.cpu_count())
[docs] def normalization(spec, n_samples, sampling_rate, fft_norm='none', inverse=False, single_sided=True, window=None): r""" Normalize a Fourier spectrum. Apply normalizations defined in [1]_ to the DFT spectrum. Note that the phase is maintained in all cases, i.e., instead of taking the squared absolute values for ``'power'`` and ``'psd'``, the complex spectra are multiplied with their absolute values to ensure a correct renormalization. Details on the FFT normalization given by `fft_norm` are given above. Parameters ---------- spec : numpy array N dimensional array which has the frequency bins in the last dimension. E.g., ``spec.shape == (10,2,129)`` holds 10 times 2 spectra with 129 frequency bins each. n_samples : int number of samples of the corresponding time signal sampling_rate : number sampling rate of the corresponding time signal in Hz fft_norm : string, optional ``'none'`` Do not apply any normalization. Appropriate for energy signals such as impulse responses. ``'unitary'`` Multiply `spec` by factor of two as in [1]_ Eq. (8) (except for 0 Hz and the Nyquist frequency at half the sampling rate) to obtain the single-sided spectrum. ``'amplitude'`` Scale spectrum by ``1/n_samples`` as in [1]_ Eq. (4) to obtain the amplitude spectrum. 'rms' Scale spectrum by :math:`1/\sqrt{2}` as in [1]_ Eq.(10) to obtain the RMS spectrum. 'power' Power spectrum, which equals the squared RMS spectrum (except for the retained phase). 'psd' The power spectrum is scaled by ``n_samples/sampling_rate`` as in [1]_ Eq. (6) Note that the `unitary` normalization is also applied for `amplitude`, `rms`, `power`, and `psd` if the input spectrum is single sided (see `single_sided`). inverse : bool, optional apply the inverse normalization. The default is ``False``. single_sided : bool, optional denotes if `spec` is a single sided spectrum up to half the sampling rate or a both sided (full) spectrum. If ``single_sided=True`` the `unitary` normalization according to [1]_ Eq. (8) is applied unless ``fft_norm='none'``. The default is ``True``. window : None, array like window that was applied to the time signal before performing the FFT. Affects the normalization as in [1]_ Eqs. (11-13). The window must be an array-like with `n_samples` length and. The default is ``None``, which denotes that no window was applied. Returns ------- spec : numpy array normalized input spectrum References ---------- .. [1] J. Ahrens, C. Andersson, P. Höstmad, and W. Kropp, “Tutorial on Scaling of the Discrete Fourier Transform and the Implied Physical Units of the Spectra of Time-Discrete Signals,” Vienna, Austria, May 2020, p. e-Brief 600. """ # check if normalization should be applied if fft_norm == 'none': return spec # check input if not isinstance(spec, np.ndarray): raise ValueError("Input 'spec' must be a numpy array.") if window is not None: if len(window) != n_samples: raise ValueError((f"window must be {n_samples} long " f"but is {len(window)} long.")) n_bins = spec.shape[-1] norm = np.ones(n_bins) # account for type of normalization if fft_norm == "amplitude": if window is None: # Equation 4 in Ahrens et al. 2020 norm /= n_samples else: # Equation 11 in Ahrens et al. 2020 norm /= np.sum(window) elif fft_norm == 'rms': if not single_sided: raise ValueError( "'rms' normalization does only exist for single-sided spectra") if window is None: # Equation 10 in Ahrens et al. 2020 norm /= n_samples else: # Equation 11 in Ahrens et al. 2020 norm /= np.sum(window) if _is_odd(n_samples): norm[1:] /= np.sqrt(2) else: norm[1:-1] /= np.sqrt(2) elif fft_norm == 'power': if window is None: # Equation 5 in Ahrens et al. 2020 norm /= n_samples**2 else: # Equation 12 in Ahrens et al. 2020 norm /= np.sum(window)**2 if not inverse: spec = np.abs(spec)**2 elif fft_norm == 'psd': if window is None: # Equation 6 in Ahrens et al. 2020 norm /= (n_samples * sampling_rate) else: # Equation 13 in Ahrens et al. 2020 norm /= (np.sum(np.asarray(window)**2) * sampling_rate) if not inverse: spec = np.abs(spec)**2 elif fft_norm != 'unitary': raise ValueError(("norm type must be 'unitary', 'amplitude', 'rms', " f"'power', or 'psd' but is '{fft_norm}'")) # account for inverse if inverse: norm = 1 / norm # apply normalization spec = spec * norm # scaling for single sided spectrum, i.e., to account for the lost # energy in the discarded half of the spectrum. Only the bins at 0 Hz # and Nyquist remain as they are (Equation 8 in Ahrens et al. 2020). if single_sided: scale = 2 if not inverse else 1 / 2 if _is_odd(n_samples): spec[..., 1:] *= scale else: spec[..., 1:-1] *= scale # reverse the squaring in case of 'power' and 'psd' normalization if inverse and fft_norm in ["power", "psd"]: spec /= np.sqrt(np.abs(spec)) return spec
def _is_odd(num): """ Check if a number is even or odd. Returns True if odd and False if even. Parameters ---------- num : int Integer number to check Returns ------- condition : bool True if odd and False if even """ return bool(num & 0x1) def _n_bins_from_n_samples(n_samples, complex_time=False): """ Helper function to calculate the number of bins resulting from a FFT with n_samples. Parameters ---------- n_samples : int Number of samples complex_time : bool Flag which indicates if the time data are real or complex-valued. Returns ------- n_bins : int Resulting number of frequency bins """ return int(n_samples) if complex_time else n_samples // 2 + 1 def _n_samples_from_n_bins(num_freq_bins, is_complex=False): """ Helper function to calculate the number of samples resulting from an inverse FFT of a spectrum with n_freq_bins. Parameters ---------- num_freq_bins : int Number of frequency bins is_complex : bool Flag which indicates if the frequency data are a one or two-sided spectrum. Returns ------- n_samples : int Resulting number of time samples. """ return num_freq_bins if is_complex else max(1, (num_freq_bins - 1) * 2) def _check_conjugate_symmetry(data): """ Check if the frequency bins are conjugate symmetric around 0 Hz. Parameters ---------- data : numpy array M-dimensional array of double-sided spectrum of shape (..., N) containing N frequency bins. The 0 Hz bin must always be at index `data.shape[-1] // 2`. Returns ------- results : bool return `True` if the fequency data are conjugate symmetric around 0 Hz, return `False` if not. """ dc_idx = data.shape[-1] // 2 if _is_odd(data.shape[-1]): mirror_spec = np.conj(np.flip(data[..., :dc_idx], axis=-1)) else: mirror_spec = np.conj(np.flip(data[..., 1:dc_idx], axis=-1)) return bool(mirror_spec.shape[-1] > 0 and np.allclose( data[..., dc_idx+1:], mirror_spec, rtol=5*np.finfo(data.dtype).eps))
[docs] def add_mirror_spectrum(data_single_sided, even_samples): """ Adds mirror spectrum to single-sided frequency data and applies fftshift. The output is a double-sided spectrum that matches the format of :py:func:`~fft`. Parameters ---------- data_single_sided : numpy array M-dimensional array of single-sided spectrum of shape (..., N) containing N frequency bins. even_samples : bool flag which indicates if the number of samples of the time data were even. Returns ------- data : numpy array M-dimensional array of double-sided spectrum of shape (..., N) containing N frequency bins. """ if even_samples: mirror_spec = data_single_sided[..., 1:-1] else: mirror_spec = data_single_sided[..., 1:] mirror_spec = np.conj(np.flip(mirror_spec, axis=-1)) data = np.concatenate((data_single_sided, mirror_spec), axis=-1) data[..., 0] = np.real(data[..., 0]) # ensure DC bin is real valued return sfft.fftshift(data, axes=-1)
[docs] def remove_mirror_spectrum(data_double_sided): """ Checks if the data are conjugate symmetric and removes the redundand mirror spectrum of double-sided frequency data. The output is a single-sided spectrum that matches the format of :py:func:`~rfft`. Parameters ---------- data_double_sided : numpy array M-dimensional array of double-sided spectrum of shape (..., N) containing N frequency bins. The 0 Hz bin must always be at index `data.shape[-1] // 2`. Returns ------- data : numpy array M-dimensional array of single-sided spectrum of shape (..., N//2+1) containing N//2+1 frequency bins. """ if _check_conjugate_symmetry(data_double_sided): N = data_double_sided.shape[-1] data_double_sided = sfft.ifftshift(data_double_sided) return data_double_sided[..., :N // 2 + 1] else: raise ValueError("Signals frequency spectrum is not" " conjugate symmetric, is_complex flag" " cannot be `False`.")