"""
Lean SNDR + ENOB computation.
Uses a direct FFT ratio path for optimization loops and spec gates. This keeps
the metric definition aligned with compute_spectrum for explicit coherent
side-bin settings without running harmonic or spur analysis. Pass
``side_bin="auto"`` to opt into the heavier auto side-bin detector used by
``compute_spectrum(..., side_bin=None)``.
"""
import numpy as np
from adctoolbox.spectrum._bin_ranges import rfft_inband_bin_count
from adctoolbox.spectrum._locate_fundamental import _locate_fundamental
from adctoolbox.spectrum._prepare_fft_input import _prepare_fft_input
from adctoolbox.spectrum._side_bin_auto import (
_detect_side_bin_auto_with_reference,
_find_hidden_components,
_warn_hidden_components,
)
from adctoolbox.spectrum._spectrum_averaging import _power_average
from adctoolbox.spectrum._window import (
_calculate_power_correction,
_create_window,
_get_auto_side_bin_fallback,
_get_default_side_bin,
)
[docs]
def quick_sndr(data, fs=1.0, win_type="hann", side_bin=None, max_scale_range=None):
"""
SNDR + ENOB from a single 1-D capture.
The SNDR definition matches analyze_spectrum for the same side_bin. The
defaults differ: here None means the window's coherent side_bin, while
analyze_spectrum/compute_spectrum default to auto detection. For a
coherent capture both agree; for a non-coherent capture pass
``side_bin="auto"`` to match analyze_spectrum, because the coherent
default counts the window leakage outside the main lobe as noise.
Parameters
----------
data
Time-domain samples, shape (N,)
fs
Sample rate (Hz)
win_type
Window name ('hann', 'rectangular', ...)
side_bin
Side bins around fundamental. None uses the window's coherent default
fast path. "auto" uses waveform-based side-bin detection matching
``compute_spectrum(..., side_bin=None)``; this adds an extra ideal-tone
FFT pass and assumes a dominant single-tone capture, and it raises a
UserWarning when the detected window also contains a spur or second
tone. For multi-tone, strongly distorted, or very noisy captures, pass
an explicit integer.
max_scale_range
Optional full-scale range for input normalization
Returns
-------
dict
``{'sndr_dbc': float, 'enob': float}``
"""
data_normalized = _prepare_fft_input(data, max_scale_range)
_, n_fft = data_normalized.shape
n_inband = rfft_inband_bin_count(n_fft, osr=1)
window_vector, window_gain, equiv_noise_bw_factor = _create_window(win_type, n_fft)
power_spectrum, _ = _power_average(data_normalized * window_vector)
power_correction = _calculate_power_correction(window_gain, equiv_noise_bw_factor)
power_spectrum *= power_correction
fundamental_bin, fundamental_bin_fractional = _locate_fundamental(power_spectrum, n_inband)
if isinstance(side_bin, str) and side_bin == "auto":
minimum_side_bin = _get_default_side_bin(win_type)
side_bin, ideal_spec, noise_floor_per_bin = _detect_side_bin_auto_with_reference(
power_spectrum,
fundamental_bin,
fundamental_bin_fractional,
n_inband,
n_fft,
window_vector,
power_correction,
fallback_side_bin=_get_auto_side_bin_fallback(win_type),
minimum_side_bin=minimum_side_bin,
)
hidden_bins = _find_hidden_components(
power_spectrum,
fundamental_bin,
side_bin,
ideal_spec,
noise_floor_per_bin,
core_half_width=minimum_side_bin + 1,
n_inband=n_inband,
)
_warn_hidden_components(
"quick_sndr", power_spectrum, fundamental_bin, side_bin,
hidden_bins, stacklevel=3,
)
elif side_bin is None:
side_bin = _get_default_side_bin(win_type)
side_bin = int(max(side_bin, 0))
sig_bin_start = max(fundamental_bin - side_bin, 0)
sig_bin_end = min(fundamental_bin + side_bin + 1, n_inband)
sig_linear = float(np.sum(power_spectrum[sig_bin_start:sig_bin_end]))
noise_distortion_spectrum = power_spectrum.copy()
noise_distortion_spectrum[: min(side_bin, len(noise_distortion_spectrum))] = 0.0
noise_distortion_spectrum[sig_bin_start:sig_bin_end] = 0.0
noise_distortion = float(np.sum(noise_distortion_spectrum[:n_inband]))
sndr_dbc = 10 * np.log10(sig_linear / (noise_distortion + 1e-20))
enob = (sndr_dbc - 1.76) / 6.02
return {"sndr_dbc": float(sndr_dbc), "enob": float(enob)}