Python Measurement API¶
doppler.measure.ToneMeasure analyses one time-domain capture (real or complex)
into the full single-tone ADC / spectral metric bag — SNR, SINAD, THD, THD+N,
SFDR, ENOB, noise floor and the worst spur — plus the accuracy/resolution
metadata. Each component's power is integrated over its window main lobe
(IEEE Std 1241); see the design guide for the
equations and conventions, and the
Spectral & Measurement API Map for how this
module composes with the shared PSD core.
Source:
src/doppler/measure/__init__.py
Examples¶
Single-tone metrics¶
import numpy as np
from doppler.measure import ToneMeasure
fs, n = 100e6, 1 << 14
x = np.cos(2 * np.pi * 10.017e6 * np.arange(n) / fs).astype(np.float32)
m = ToneMeasure(n=n, fs=fs) # auto Kaiser window, sized from bits/DR
r = m.analyze(x) # named ToneMetrics result
r.enob, r.sfdr_dbc, r.fund_dbfs # attribute access
snr, sinad, *_ = r # ...and tuple unpacking
ENOB of an ADC¶
from doppler.cvt import ADC
codes = ADC(12, 0.0, 0).steps(x).astype(np.float32)
m = ToneMeasure(n=n, fs=fs, bits=12) # bits sets the dBFS reference
print(round(m.analyze(codes).enob, 2)) # ≈ 12.0 for an ideal 12-bit ADC
All three analyzers take the bits (ADC depth → 2**(bits-1)) or
full_scale dBFS knob and read it back from the shared PSD
core, so bits=B is identical to full_scale=2**(B-1) — one source of truth.
Each also exposes spectrum_dbfs(x): the same averaged-PSD dBFS trace its
metrics use, for a display backdrop (no hand-rolled periodogram needed).
Complex baseband, accuracy metadata, and the spectrum¶
iq = np.exp(2j * np.pi * 13e6 * np.arange(n) / fs).astype(np.complex64)
r = m.analyze_complex(iq) # two-sided analysis
m.rbw, m.bin_hz, r.lobe_bins # resolution vs interpolation grid
spec = m.spectrum_dbfs(x) # DC-centred dBFS trace (length nfft) for plots
ts = m.time_stats(x) # crest_db / papr_db / dc_offset / fs_util_pct
Resolution vs bin spacing
m.rbw (resolution bandwidth) is derived from the un-padded length n;
m.bin_hz is the zero-padded interpolation grid. Padding sharpens the
frequency estimate and the plot, but does not improve resolution.
Two-tone IMD and notched-noise NPR¶
from doppler.measure import IMDMeasure, NPRMeasure
# Two equal tones -> IMD2/IMD3 and the third-order intercept
t = np.arange(n)
two_tone_capture = (
0.5 * np.cos(2 * np.pi * 10.0e6 * t / fs)
+ 0.5 * np.cos(2 * np.pi * 11.0e6 * t / fs)
).astype(np.float32)
imd = IMDMeasure(n=n, fs=fs)
r = imd.analyze(two_tone_capture) # r.imd3_dbc, r.toi_dbfs, ...
# Notched-noise loading -> NPR (band/notch geometry are analyze() args)
noise = np.random.randn(n).astype(np.float32)
active_lo, active_hi = 1.0e6, 40.0e6 # loaded band edges (Hz)
notch_lo, notch_hi = 19.0e6, 21.0e6 # the cleared notch (Hz)
guard_hz = 1.0e6
npr = NPRMeasure(n=n, fs=fs, bits=10)
g = npr.analyze(noise, active_lo, active_hi, notch_lo, notch_hi, guard_hz)
print(g.npr_db)
# both expose the same display-spectrum method as ToneMeasure
imd_spec = imd.spectrum_dbfs(two_tone_capture) # DC-centred dBFS, length nfft
npr_spec = npr.spectrum_dbfs(noise)
Capture planning¶
from doppler.measure import (
dp_coherent_freq,
measure_min_samples,
measure_proc_gain,
measure_rec_nfft,
)
n = measure_min_samples(
fs, target_rbw=1e3, bits=12, dynamic_range_db=0.0, complex_input=0
)
nfft = measure_rec_nfft(n, pad=2)
pg = measure_proc_gain(nfft)
f0 = dp_coherent_freq(fs, 10e6, n) # leakage-free coherent test tone
ToneMeasure
¶
Create a ToneMeasure analyser (auto Kaiser window).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Capture/frame length (>= 2). |
8192
|
fs
|
float
|
Sample rate (Hz, > 0). |
1.0
|
n_harmonics
|
int
|
Harmonics to track (k = 2..n_harmonics). |
8
|
full_scale
|
float
|
Amplitude that equals 0 dBFS (> 0). Ignored if bits > 0. |
1.0
|
bits
|
int
|
ADC depth: bits>0 sets the 0-dBFS reference to 2^(bits-1) and, unless overridden, the dynamic-range target (6.02*bits + 1.76 + headroom). |
0
|
dynamic_range_db
|
float
|
Explicit sidelobe/dynamic-range target (dB); used when > 0, else derived from bits (or a deep default when both are 0). |
0.0
|
dc_guard
|
int
|
Extra bins excluded beyond L around DC. |
0
|
Examples:
Create with defaults:
>>> from doppler.measure import ToneMeasure
>>> obj = ToneMeasure(n=8192, fs=1.0, n_harmonics=8, full_scale=1.0, bits=0, dynamic_range_db=0.0, dc_guard=0)
analyze
¶
analyze(x: float) -> tuple[float, float, float, float, float, float, float, float, float, float, float, float, float, float, int, float, float, float, int, int, float, float, float]
Analyze a real time-domain capture; returns a ToneMetrics result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float, float, float, float, float, float, float, float, float, float, float, float, float, int, float, float, float, int, int, float, float, float]
|
the metric record (by value). |
Examples:
>>> from doppler.measure import ToneMeasure
>>> import numpy as np
>>> n, t = 4096, np.arange(4096)
>>> # full-scale tone at 300 cycles + a 2nd harmonic 40 dB down
>>> x = (np.cos(2*np.pi*300*t/n)
... + 0.01*np.cos(2*np.pi*600*t/n)).astype(np.float32)
>>> r = ToneMeasure(n=n, fs=1.0).analyze(x)
>>> type(r).__name__
'ToneMetrics'
>>> abs(r.fund_dbfs) < 0.1, round(r.thd, 1) # 0 dBFS tone, THD -40 dBc
(True, -40.0)
analyze_complex
¶
analyze_complex(x: complex) -> tuple[float, float, float, float, float, float, float, float, float, float, float, float, float, float, int, float, float, float, int, int, float, float, float]
Analyze a complex baseband capture (two-sided spectrum).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float, float, float, float, float, float, float, float, float, float, float, float, float, int, float, float, float, int, int, float, float, float]
|
Output. |
Examples:
time_stats
¶
Time-domain stats: RMS, peak, crest/PAPR, DC offset, FS utilisation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float, float, float, float, float]
|
Output. |
Examples:
spectrum_dbfs
¶
DC-centred dBFS magnitude spectrum of a capture (length nfft, for plots).
Without out=, the returned array is a view into a buffer reused on the next call (see spectrum_dbfs_max_out() to size an out= buffer for an independent, alias-free result).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float32]
|
Input. |
required |
out
|
NDArray[float32]
|
Caller-provided output buffer, at least max(spectrum_dbfs_max_out(), len(x)) elements. |
...
|
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
Number of samples written (nfft). |
spectrum_dbfs_max_out
¶
Max output length spectrum_dbfs() can produce for the current state. Use to size the out= buffer.
IMDMeasure
¶
Create an IMDMeasure analyser (auto Kaiser window).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Capture/frame length (>= 2). |
8192
|
fs
|
float
|
Sample rate (Hz, > 0). |
1.0
|
full_scale
|
float
|
Amplitude that equals 0 dBFS (> 0). Ignored if bits > 0. |
1.0
|
bits
|
int
|
ADC depth: bits>0 sets the 0-dBFS reference to 2^(bits-1) and, unless overridden, the dynamic-range target. |
0
|
dynamic_range_db
|
float
|
Explicit sidelobe/dynamic-range target (dB); used when > 0, else derived from bits. |
0.0
|
Examples:
Create with defaults:
>>> from doppler.measure import IMDMeasure
>>> obj = IMDMeasure(n=8192, fs=1.0, full_scale=1.0, bits=0, dynamic_range_db=0.0)
analyze
¶
analyze(x: float) -> tuple[float, float, float, float, float, float, float, float, float, float, float, float]
Two-tone IMD/TOI of a real capture (finds the two strongest tones).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float, float, float, float, float, float, float, float, float, float, float]
|
the IMD metric record (by value; zeroed if no two tones are found). |
Examples:
>>> from doppler.measure import IMDMeasure
>>> import numpy as np
>>> t = np.arange(4096)
>>> # two equal tones at 200 & 250 cycles + 3rd-order products 40 dB down
>>> x = (np.cos(2*np.pi*200*t/4096) + np.cos(2*np.pi*250*t/4096)
... + 0.01*np.cos(2*np.pi*150*t/4096)
... + 0.01*np.cos(2*np.pi*300*t/4096)).astype(np.float32)
>>> r = IMDMeasure(n=4096, fs=1.0).analyze(x)
>>> round(r.f1, 4), round(r.f2, 4), round(r.imd3_dbc, 0)
(0.0488, 0.061, -40.0)
spectrum_dbfs
¶
DC-centred dBFS magnitude spectrum of a capture (length nfft, for plots).
Without out=, the returned array is a view into a buffer reused on the next call (see spectrum_dbfs_max_out() to size an out= buffer for an independent, alias-free result).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float32]
|
Input. |
required |
out
|
NDArray[float32]
|
Caller-provided output buffer, at least max(spectrum_dbfs_max_out(), len(x)) elements. |
...
|
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
Output. |
spectrum_dbfs_max_out
¶
Max output length spectrum_dbfs() can produce for the current state. Use to size the out= buffer.
NPRMeasure
¶
Create an NPRMeasure analyser (auto Kaiser window).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Capture/frame length (>= 2). |
8192
|
fs
|
float
|
Sample rate (Hz, > 0). |
1.0
|
full_scale
|
float
|
Amplitude that equals 0 dBFS (> 0). Ignored if bits > 0. |
1.0
|
bits
|
int
|
ADC depth: bits>0 sets the 0-dBFS reference to 2^(bits-1) and, unless overridden, the dynamic-range target. |
0
|
dynamic_range_db
|
float
|
Explicit sidelobe/dynamic-range target (dB); used when > 0, else derived from bits. |
0.0
|
Examples:
Create with defaults:
>>> from doppler.measure import NPRMeasure
>>> obj = NPRMeasure(n=8192, fs=1.0, full_scale=1.0, bits=0, dynamic_range_db=0.0)
analyze
¶
analyze(x: float, active_lo: float, active_hi: float, notch_lo: float, notch_hi: float, guard_hz: float = 0.0) -> tuple[float, float, float, int, int, float]
NPR of a notched-noise capture over [active_lo,active_hi] with a notch [notch_lo,notch_hi] (Hz) and guard keep-out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
Real time-domain capture. |
required |
active_lo
|
float
|
Active noise band lower edge (Hz). |
required |
active_hi
|
float
|
Active noise band upper edge (Hz). |
required |
notch_lo
|
float
|
Notch lower edge (Hz). |
required |
notch_hi
|
float
|
Notch upper edge (Hz). |
required |
guard_hz
|
float
|
Keep-out around the notch edges (Hz). |
0.0
|
Returns:
| Type | Description |
|---|---|
tuple[float, float, float, int, int, float]
|
the NPR metric record (by value). |
Examples:
>>> from doppler.measure import NPRMeasure
>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> n = 1 << 15
>>> F = np.fft.rfft(rng.standard_normal(n))
>>> f = np.fft.rfftfreq(n)
>>> F[(f < 0.05) | (f > 0.45)] = 0 # band-limit to [0.05,0.45]
>>> F[(f >= 0.20) & (f <= 0.25)] *= 10**(-50/20) # notch 50 dB deep
>>> x = np.fft.irfft(F, n)
>>> x = (0.3*x/np.std(x)).astype(np.float32)
>>> r = NPRMeasure(n=n, fs=1.0).analyze(x, 0.05, 0.45, 0.20, 0.25, 0.01)
>>> 45 < r.npr_db < 55, r.notch_psd_dbfs < r.inband_psd_dbfs
(True, True)
spectrum_dbfs
¶
DC-centred dBFS magnitude spectrum of a capture (length nfft, for plots).
Without out=, the returned array is a view into a buffer reused on the next call (see spectrum_dbfs_max_out() to size an out= buffer for an independent, alias-free result).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float32]
|
Input. |
required |
out
|
NDArray[float32]
|
Caller-provided output buffer, at least max(spectrum_dbfs_max_out(), len(x)) elements. |
...
|
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
Output. |
spectrum_dbfs_max_out
¶
Max output length spectrum_dbfs() can produce for the current state. Use to size the out= buffer.
measure_min_samples
¶
measure_min_samples(fs: float, target_rbw: float, bits: int, dynamic_range_db: float, complex_input: int) -> int
Samples for a target RBW (auto Kaiser from bits/dynamic_range_db; target_rbw<=0 -> span/1000).
Plans a capture for the same auto-Kaiser window the measurement objects use: the dynamic-range target (from dynamic_range_db, else bits) selects the Kaiser beta, whose ENBW (measured via kaiser_enbw) sets the bins-per-RBW. RBW = ENBW * fs / n, so n = ceil(ENBW * fs / target_rbw).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fs
|
float
|
Sample rate (Hz, > 0). |
required |
target_rbw
|
float
|
Desired resolution bandwidth (Hz). When <= 0 it defaults to span/1000, where span = fs/2 for real captures and fs for complex (complex_input). |
required |
bits
|
int
|
ADC depth: sets the dynamic-range target when no explicit override is given. |
required |
dynamic_range_db
|
float
|
Explicit dynamic-range target (dB); used when > 0. |
required |
complex_input
|
int
|
Non-zero if the capture is complex (span = fs). |
required |
Returns:
| Type | Description |
|---|---|
int
|
Required capture length, or 0 on bad args. |
measure_rec_nfft
¶
Recommended zero-padded transform length: next_pow2(n * pad).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Input. |
required |
pad
|
int
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Output. |
measure_proc_gain
¶
FFT processing gain in dB: 10*log10(nfft / 2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nfft
|
int
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Output. |
dp_coherent_freq
¶
Nearest leakage-free coherent test frequency (J cycles, J coprime N).
Snaps f_target to J * fs / N where J is the nearest integer cycle
count that is coprime with N — an integer number of cycles in the
capture (no leakage) with J coprime to N (so quantisation-noise
correlation is minimised).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fs
|
float
|
Input. |
required |
f_target
|
float
|
Input. |
required |
N
|
int
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The coherent frequency (Hz), or 0 on bad args. |