Skip to content

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)

n property

n: int

N.

nfft property

nfft: int

Nfft.

fs property

fs: float

Fs.

enbw property

enbw: float

Enbw.

lobe_bins property

lobe_bins: int

Lobe bins.

spur_guard_bins property

spur_guard_bins: int

Spur guard bins.

beta property

beta: float

Beta.

rbw property

rbw: float

Rbw.

bin_hz property

bin_hz: float

Bin hz.

proc_gain_db property

proc_gain_db: float

Proc gain db.

reset

reset() -> None

Reset (no-op: the analyser is stateless between calls).

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:

>>> from doppler.measure import ToneMeasure
>>> import numpy as np
>>> i = np.arange(4096)
>>> x = np.exp(2j*np.pi*137*i/4096).astype(np.complex64)
>>> r = ToneMeasure(n=4096, fs=1.0).analyze_complex(x)
>>> round(r.fund_freq, 4), abs(r.fund_dbfs) < 0.2
(0.0334, True)

time_stats

time_stats(x: float) -> tuple[float, float, float, float, float, float]

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:

>>> from doppler.measure import ToneMeasure
>>> import numpy as np
>>> t = np.arange(4096)
>>> x = (0.8*np.cos(2*np.pi*50*t/4096)).astype(np.float32)
>>> ts = ToneMeasure(n=4096, fs=1.0).time_stats(x)
>>> round(ts.crest_db, 2), round(ts.fs_util_pct, 0)   # sine crest ~3.01 dB
(3.01, 80.0)

spectrum_dbfs

spectrum_dbfs(x: NDArray[float32], out: NDArray[float32] | None = ...) -> NDArray[np.float32]

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

spectrum_dbfs_max_out() -> int

Max output length spectrum_dbfs() can produce for the current state. Use to size the out= buffer.

destroy

destroy() -> None

Release C resources immediately.


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)

n property

n: int

N.

nfft property

nfft: int

Nfft.

fs property

fs: float

Fs.

reset

reset() -> None

Reset (no-op: each analyze() call is independent).

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

spectrum_dbfs(x: NDArray[float32], out: NDArray[float32] | None = ...) -> NDArray[np.float32]

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

spectrum_dbfs_max_out() -> int

Max output length spectrum_dbfs() can produce for the current state. Use to size the out= buffer.

destroy

destroy() -> None

Release C resources immediately.


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)

n property

n: int

N.

nfft property

nfft: int

Nfft.

fs property

fs: float

Fs.

rbw property

rbw: float

Rbw.

reset

reset() -> None

Reset (no-op: each analyze() call is independent).

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

spectrum_dbfs(x: NDArray[float32], out: NDArray[float32] | None = ...) -> NDArray[np.float32]

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

spectrum_dbfs_max_out() -> int

Max output length spectrum_dbfs() can produce for the current state. Use to size the out= buffer.

destroy

destroy() -> None

Release C resources immediately.


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

measure_rec_nfft(n: int, pad: int) -> int

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

measure_proc_gain(nfft: int) -> float

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

dp_coherent_freq(fs: float, f_target: float, N: int) -> float

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.