Source code for sigima.tools.signal.dynamic

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
.. Dynamic Parameters (see parent package :mod:`sigima.tools.signal`)

"""

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...

from __future__ import annotations

import warnings

import numpy as np
import scipy.optimize

from sigima.enums import PowerUnit
from sigima.tools.checks import check_1d_arrays


[docs] def sinusoidal_model( x: np.ndarray, a: float, f: float, phi: float, offset: float ) -> np.ndarray: """Sinusoidal model function.""" return a * np.sin(2 * np.pi * f * x + phi) + offset
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def sinusoidal_fit( x: np.ndarray, y: np.ndarray ) -> tuple[tuple[float, float, float, float], float]: """Fit a sinusoidal model to the input data. Args: x: X data y: Y data Returns: A tuple containing the fit parameters (amplitude, frequency, phase, offset) and the residuals """ # Initial guess for the parameters # ================================================================================== offset = np.mean(y) amp = (np.max(y) - np.min(y)) / 2 phase_origin = 0 # Search for the maximum of the FFT i_maxfft = np.argmax(np.abs(np.fft.fft(y - offset))) if i_maxfft > len(x) / 2: # If the index is greater than N/2, we are in the mirrored half spectrum # (negative frequencies) i_maxfft = len(x) - i_maxfft freq = i_maxfft / (x[-1] - x[0]) # ================================================================================== def optfunc(fitparams: np.ndarray, x: np.ndarray, y: np.ndarray) -> np.ndarray: """Optimization function.""" return y - sinusoidal_model(x, *fitparams) # Fit the model to the data fitparams = scipy.optimize.leastsq( optfunc, [amp, freq, phase_origin, offset], args=(x, y) )[0] y_th = sinusoidal_model(x, *fitparams) residuals = np.std(y - y_th) return fitparams, residuals
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def sinus_frequency(x: np.ndarray, y: np.ndarray) -> float: """Compute the frequency of a sinusoidal signal. Args: x: x signal data y: y signal data Returns: Frequency of the sinusoidal signal """ fitparams, _residuals = sinusoidal_fit(x, y) return fitparams[1]
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def enob(x: np.ndarray, y: np.ndarray, full_scale: float = 1.0) -> float: """Compute Effective Number of Bits (ENOB). Args: x: x signal data y: y signal data full_scale: Full scale(V). Defaults to 1.0. Returns: Effective Number of Bits (ENOB) """ _fitparams, residuals = sinusoidal_fit(x, y) return -np.log2(residuals * np.sqrt(12) / full_scale)
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def sinad( x: np.ndarray, y: np.ndarray, full_scale: float = 1.0, unit: PowerUnit = PowerUnit.DBC, ) -> float: """Compute Signal-to-Noise and Distortion Ratio (SINAD). Args: x: x signal data y: y signal data full_scale: Full scale(V). Defaults to 1.0. unit: Unit of the input data. Defaults to PowerUnit.DBC. Returns: Signal-to-Noise and Distortion Ratio (SINAD) """ fitparams, residuals = sinusoidal_fit(x, y) amp = fitparams[0] # Compute the power of the fundamental if unit == PowerUnit.DBC: powf = np.abs(amp / np.sqrt(2)) else: powf = full_scale / (2 * np.sqrt(2)) return 20 * np.log10(powf / residuals)
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def thd( x: np.ndarray, y: np.ndarray, full_scale: float = 1.0, unit: PowerUnit = PowerUnit.DBC, nb_harm: int = 5, ) -> float: """Compute Total Harmonic Distortion (THD). Args: x: x signal data y: y signal data full_scale: Full scale(V). Defaults to 1.0. unit: Unit of the input data. Defaults to PowerUnit.DBC. nb_harm: Number of harmonics to consider. Defaults to 5. Returns: Total Harmonic Distortion (THD) """ fitparams, _residuals = sinusoidal_fit(x, y) offset = np.mean(y) amp, freq = fitparams[:2] ampfft = np.abs(np.fft.fft(y - offset)) # Compute the power of the fundamental if unit == PowerUnit.DBC: powfund = np.max(ampfft[: len(ampfft) // 2]) else: powfund = (full_scale / (2 * np.sqrt(2))) * (len(x) / np.sqrt(2)) sumharm = 0 for i in np.arange(nb_harm + 2)[2:]: a = i * np.ceil(freq * (x[-1] - x[0])) amp = ampfft[int(a - 5) : int(a + 5)] if len(amp) > 0: sumharm += np.max(amp) return 20 * np.log10(sumharm / powfund)
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def sfdr( x: np.ndarray, y: np.ndarray, full_scale: float = 1.0, unit: PowerUnit = PowerUnit.DBC, ) -> float: """Compute Spurious-Free Dynamic Range (SFDR). Args: x: x signal data y: y signal data full_scale: Full scale(V). Defaults to 1.0. unit: Unit of the input data. Defaults to PowerUnit.DBC. Returns: Spurious-Free Dynamic Range (SFDR) """ fitparams, _residuals = sinusoidal_fit(x, y) # Compute the power of the fundamental if unit == PowerUnit.DBC: powfund = np.max(np.abs(np.fft.fft(y))) else: powfund = (full_scale / (2 * np.sqrt(2))) * (len(x) / np.sqrt(2)) maxspike = np.max(np.abs(np.fft.fft(y - sinusoidal_model(x, *fitparams)))) return 20 * np.log10(powfund / maxspike)
[docs] @check_1d_arrays(x_evenly_spaced=True, x_sorted=True) def snr( x: np.ndarray, y: np.ndarray, full_scale: float = 1.0, unit: PowerUnit = PowerUnit.DBC, ) -> float: """Compute Signal-to-Noise Ratio (SNR). Args: x: x signal data y: y signal data full_scale: Full scale(V). Defaults to 1.0. unit: Unit of the input data. Defaults to PowerUnit.DBC. Returns: Signal-to-Noise Ratio (SNR) """ fitparams, _residuals = sinusoidal_fit(x, y) # Compute the power of the fundamental if unit == PowerUnit.DBC: powfund = np.max(np.abs(np.fft.fft(y))) else: powfund = (full_scale / (2 * np.sqrt(2))) * (len(x) / np.sqrt(2)) noise = np.sqrt(np.mean((y - sinusoidal_model(x, *fitparams)) ** 2)) return 20 * np.log10(powfund / noise)
[docs] def sampling_period(x: np.ndarray) -> float: """Compute sampling period Args: x: X data Returns: Sampling period """ steps = np.diff(x) if not np.isclose(np.diff(steps).max(), 0, atol=1e-10): warnings.warn("Non-constant sampling signal") return steps[0]
[docs] def sampling_rate(x: np.ndarray) -> float: """Compute mean sampling rate Args: x: X data Returns: Sampling rate """ return 1.0 / sampling_period(x)