"""Error spectrum analysis.
Analyzes the spectrum of the fitting error signal directly to reveal
frequency components and error characteristics.
"""
import matplotlib.pyplot as plt
from adctoolbox.spectrum import analyze_spectrum
from adctoolbox.fundamentals.fit_sine_4param import fit_sine_4param
from adctoolbox.aout._fit_diagnostics import extract_fit_diagnostics
[docs]
def analyze_error_spectrum(signal, fs=1, frequency=None, create_plot: bool = True,
ax=None, title: str = None, max_iterations: int = 1,
tolerance: float = 1e-9, return_fit: bool = False,
max_scale_range=None):
"""
Compute error spectrum directly from the error signal.
This function fits an ideal sine to the signal, computes the error,
and analyzes the spectrum of the error signal (not envelope).
Parameters
----------
signal : np.ndarray
ADC output signal (1D array)
fs : float, default=1
Sampling frequency in Hz
frequency : float, optional
Normalized frequency (0-0.5). If None, auto-detected
create_plot : bool, default=True
If True, plot the error spectrum on current axes
ax : matplotlib.axes.Axes, optional
Axes to plot on. If None, uses current axes (plt.gca())
title : str, optional
Title for the plot. If None, no title is set
max_iterations : int, default=1
Frequency-refinement iterations passed to fit_sine_4param.
tolerance : float, default=1e-9
Frequency-refinement convergence threshold passed to fit_sine_4param.
return_fit : bool, default=False
If True, include scalar sine-fit diagnostics under result['fit'].
max_scale_range : float or tuple/list, optional
Full-scale reference passed to analyze_spectrum for residual scaling.
If None, the residual is self-normalized, which is useful for viewing
frequency fingerprints but does not preserve ADC full-scale dBFS
meaning. Pass the ADC range, for example (0, 1), to report the
residual spectrum relative to ADC full scale.
Returns
-------
result : dict
Dictionary containing spectrum analysis results:
- 'enob': Effective Number of Bits
- 'sndr_db': Signal-to-Noise and Distortion Ratio (dB)
- 'sfdr_db': Spurious-Free Dynamic Range (dB)
- 'snr_db': Signal-to-Noise Ratio (dB)
- 'thd_db': Total Harmonic Distortion (dB)
- 'sig_pwr_dbfs': Signal power (dBFS)
- 'noise_floor_dbfs': Noise floor (dBFS)
- 'error_signal': Error signal (signal - fitted sine)
- 'error_spectrum_scale': 'residual' when max_scale_range is None,
otherwise 'adc_full_scale'
- 'error_spectrum_max_scale_range': Full-scale reference used for
residual spectrum analysis
- 'fit': Optional sine-fit diagnostics when return_fit=True
Notes
-----
- Error = signal - ideal_sine (fitted using fit_sine_4param)
- Analyzes spectrum of error directly (no envelope extraction)
- Reveals frequency components in the error signal
- With max_scale_range=None, the residual is normalized to its own peak
range. Pass an ADC full-scale range when dBFS, noise floor, or NSD
should retain engineering full-scale meaning.
"""
# Fit ideal sine to extract reference
fit_kwargs = {"max_iterations": max_iterations, "tolerance": tolerance}
if frequency is None:
fit_result = fit_sine_4param(signal, **fit_kwargs)
else:
fit_result = fit_sine_4param(signal, frequency_estimate=frequency, **fit_kwargs)
sig_ideal = fit_result['fitted_signal']
# Compute error
error_signal = signal - sig_ideal
error_scale = "residual" if max_scale_range is None else "adc_full_scale"
ylabel = "Error Spectrum (dB, residual FS)" if max_scale_range is None else "Error Spectrum (dBFS)"
# Analyze error spectrum directly (not envelope)
if create_plot:
# Use provided axes or set current axes
if ax is not None:
plt.sca(ax)
result = analyze_spectrum(
error_signal,
fs=fs,
max_scale_range=max_scale_range,
show_label=False,
max_harmonic=5,
)
plt.xlabel("Frequency (Hz)")
plt.ylabel(ylabel)
plt.grid(True, alpha=0.3)
# Set title if provided
if title is not None:
plt.gca().set_title(title, fontsize=10, fontweight='bold')
else:
# Analyze without plotting
import matplotlib
backend_orig = matplotlib.get_backend()
matplotlib.use('Agg') # Non-interactive backend
result = analyze_spectrum(
error_signal,
fs=fs,
max_scale_range=max_scale_range,
show_label=False,
max_harmonic=5,
)
plt.close()
matplotlib.use(backend_orig) # Restore original backend
# Add error signal to results
result['error_signal'] = error_signal
result['error_spectrum_scale'] = error_scale
result['error_spectrum_max_scale_range'] = max_scale_range
if return_fit:
result['fit'] = extract_fit_diagnostics(fit_result)
return result