Benchmark phasor_from_signal#
Benchmark the phasor_from_signal function.
The phasor_from_signal() function used to calculate
phasor coordinates from time-resolved or spectral signals can operate in
two modes:
using an internal Cython function optimized for calculating a small number of harmonics, optionally using multiple threads.
using a real forward Fast Fourier Transform (FFT),
numpy.fft.rfftor a drop-in replacement function likescipy.fft.rfftormkl_fft.interfaces.numpy_fft.rfft.
This tutorial compares the performance of the two modes.
Import required modules and functions:
from timeit import timeit
import numpy
from numpy.fft import rfft as numpy_fft # noqa: F401
from phasorpy.phasor import phasor_from_signal # noqa: F401
from phasorpy.utils import number_threads
try:
from scipy.fft import rfft as scipy_fft
except ImportError:
scipy_fft = None
try:
from mkl_fft.interfaces.numpy_fft import rfft as mkl_fft
except ImportError:
mkl_fft = None
rng = numpy.random.default_rng(42) # initialize random number generator
Run benchmark#
Create a random signal with a size and dtype similar to real-world data:
signal = rng.random((384, 384, 384))
signal += 1.1
signal *= 3723 # ~12 bit
signal = signal.astype(numpy.uint16) # 108 MB
signal[signal < 4282] = 0.0 # ~5% no signal
Print execution times depending on FFT function, axis, number of harmonics, and number of threads:
statement = """
phasor_from_signal(signal, axis=axis, harmonic=harmonic, **kwargs)
"""
number = 1 # how many times to execute statement
ref = None # reference duration
def print_(descr, t):
print(f' {descr:20s}{t / number:>6.3f}s {t / ref:>6.2f}')
for harmonic in ([1], [1, 2, 3, 4, 5, 6, 7, 8]):
print(f'harmonics {len(harmonic)}')
for axis in (-1, 0, 2):
print(f' axis {axis}')
kwargs = {'use_fft': False, 'num_threads': 1}
t = timeit(statement, number=number, globals=globals())
if ref is None:
ref = t
print_('not_fft', t)
num_threads = number_threads(0, 6)
if num_threads > 1:
kwargs = {'use_fft': False, 'num_threads': num_threads}
t = timeit(statement, number=number, globals=globals())
print_(f'not_fft ({num_threads} threads)', t)
for fft_name in ('numpy_fft', 'scipy_fft', 'mkl_fft'):
fft_func = globals()[fft_name]
if fft_func is None:
continue
kwargs = {'use_fft': True, 'rfft': fft_func}
t = timeit(statement, number=number, globals=globals())
print_(f'{fft_name}', t)
harmonics 1
axis -1
not_fft 0.059s 1.00
not_fft (2 threads) 0.030s 0.51
numpy_fft 0.282s 4.74
scipy_fft 0.270s 4.55
mkl_fft 0.151s 2.55
axis 0
not_fft 0.291s 4.90
not_fft (2 threads) 0.149s 2.51
numpy_fft 0.743s 12.50
scipy_fft 0.624s 10.51
mkl_fft 0.305s 5.14
axis 2
not_fft 0.060s 1.00
not_fft (2 threads) 0.030s 0.51
numpy_fft 0.283s 4.76
scipy_fft 0.269s 4.54
mkl_fft 0.146s 2.45
harmonics 8
axis -1
not_fft 0.474s 7.97
not_fft (2 threads) 0.236s 3.97
numpy_fft 0.294s 4.95
scipy_fft 0.284s 4.78
mkl_fft 0.159s 2.67
axis 0
not_fft 2.229s 37.53
not_fft (2 threads) 1.317s 22.18
numpy_fft 0.753s 12.67
scipy_fft 0.632s 10.64
mkl_fft 0.309s 5.19
axis 2
not_fft 0.477s 8.02
not_fft (2 threads) 0.237s 3.98
numpy_fft 0.294s 4.95
scipy_fft 0.281s 4.74
mkl_fft 0.169s 2.84
For reference, the results on a Core i7-14700K CPU, Windows 11, Python 3.15.0, numpy 2.5.4 scipy 1.18.1, mkl_fft 2.3.2:
harmonics 1
axis -1
not_fft 0.047s 1.00
not_fft (6 threads) 0.008s 0.18
numpy_fft 0.266s 5.62
scipy_fft 0.199s 4.21
mkl_fft 0.123s 2.61
axis 0
not_fft 0.232s 4.91
not_fft (6 threads) 0.042s 0.88
numpy_fft 0.684s 14.47
scipy_fft 0.470s 9.95
mkl_fft 0.174s 3.67
axis 2
not_fft 0.058s 1.23
not_fft (6 threads) 0.008s 0.17
numpy_fft 0.268s 5.67
scipy_fft 0.195s 4.14
mkl_fft 0.124s 2.63
harmonics 8
axis -1
not_fft 0.385s 8.15
not_fft (6 threads) 0.060s 1.27
numpy_fft 0.276s 5.84
scipy_fft 0.212s 4.48
mkl_fft 0.148s 3.14
axis 0
not_fft 1.540s 32.59
not_fft (6 threads) 0.363s 7.68
numpy_fft 0.699s 14.80
scipy_fft 0.474s 10.03
mkl_fft 0.181s 3.82
axis 2
not_fft 0.384s 8.12
not_fft (6 threads) 0.062s 1.30
numpy_fft 0.276s 5.84
scipy_fft 0.210s 4.44
mkl_fft 0.147s 3.12
Results#
Using the Cython implementation is significantly faster than using the
numpy.fft-based implementation for single harmonics.Using multiple threads can significantly speed up the Cython mode.
The FFT functions from
scipyandmkl_fftoutperform thenumpy.fftfunction. Specifically,mkl_fftis very performant.Using FFT becomes more competitive when calculating a larger number of harmonics.
Computing over the last axis is significantly faster compared to the first axis. That is because the samples in the last dimension are contiguous in memory.
These timings are illustrative, not a controlled benchmark: each configuration was measured once on a single randomly generated dataset during a CI or documentation build. Results may vary with system load, hardware, and software environment.
Conclusions#
Using the Cython implementation is a reasonable default when calculating a few harmonics. Using FFT is a better choice when computing a large number of harmonics, especially with an optimized FFT function.
Total running time of the script: (0 minutes 12.212 seconds)