Source code for pyfar.dsp.fft

"""
The following documents the FFT functionality. More details and background is
given in the :py:mod:`FFT concepts <pyfar._concepts.fft>`.
"""
import multiprocessing

import numpy as np
from scipy import fft


[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 fft.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. 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 = fft.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. 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=True) # Inverse DFT data = fft.irfft( spec, n=n_samples, axis=-1, workers=multiprocessing.cpu_count()) return data
[docs] def normalization(spec, n_samples, sampling_rate, fft_norm='none', inverse=False, single_sided=True, window=None): """ 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. For detailed information and explanations, refer to :py:mod:`FFT concepts <pyfar._concepts.fft>`. 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 # the phase is kept for being able to switch between normalizations # altoug the power spectrum does usually not have phase information, # i.e., spec = np.abs(spec)**2 if not inverse: spec *= np.abs(spec) 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) # the phase is kept for being able to switch between normalizations # altoug the power spectrum does usually not have phase information, # i.e., spec = np.abs(spec)**2 if not inverse: spec *= np.abs(spec) 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(n_samples): """ Helper function to calculate the number of bins resulting from a FFT with n_samples Paramters --------- n_samples : int Number of samples Returns ------- n_bins : int Resulting number of frequency bins """ if _is_odd(n_samples): n_bins = (n_samples+1)/2 else: n_bins = n_samples/2+1 return int(n_bins)