Skip to content

Measurements

Spectral and baseband signal measurement utilities

EVM(x, ref)

Calculate the Error Vector Magnitude (EVM) of an input sequence.

Parameters:

Name Type Description Default
x ndarray

Sample data vector

required
ref ndarray

Reference decision data vector

required

Returns:

Type Description
floating

EVM

Source code in rfproto/measurements.py
 9
10
11
12
13
14
15
16
17
18
19
def EVM(x: np.ndarray, ref: np.ndarray) -> np.floating:
    """Calculate the Error Vector Magnitude (EVM) of an input sequence.

    Args:
        x: Sample data vector
        ref: Reference decision data vector

    Returns:
        EVM
    """
    return np.std(x - ref) / np.std(ref)

FFT_process_gain(M)

The theoretical noise floor of the FFT is equal to the theoretical SNR plus the FFT process gain, \(10\log{M/2}\). It is important to remember that the value for noise used in the SNR calculation is the noise that extends over the entire Nyquist bandwidth (DC to \(f_{s}/2\)), but the FFT acts as a narrowband spectrum analyzer with a bandwidth of \(f_{s}/M\) that sweeps over the spectrum. This has the effect of pushing the noise down by an amount equal to the process gain— the same effect as narrowing the bandwidth of an analog spectrum analyzer. Thus to find the "real" RMS noise level (which is affected by quantization, system or environmental noise), subtract the measured FFT noise floor by this processing gain value.

References:

Parameters:

Name Type Description Default
M int

Number of FFT bins

required

Returns:

Name Type Description
y float

FFT processing gain (dB)

Source code in rfproto/measurements.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def FFT_process_gain(M: int) -> float:
    """The theoretical noise floor of the FFT is equal to the theoretical SNR
    plus the FFT process gain, $10\\log{M/2}$. It is important to remember that
    the value for noise used in the SNR calculation is the noise that extends
    over the entire Nyquist bandwidth (DC to $f_{s}/2$), but the FFT acts as a
    narrowband spectrum analyzer with a bandwidth of $f_{s}/M$ that sweeps over the
    spectrum. This has the effect of pushing the noise down by an amount equal
    to the process gain— the same effect as narrowing the bandwidth of an analog
    spectrum analyzer. Thus to find the "real" RMS noise level (which is affected
    by quantization, system or environmental noise), subtract the measured FFT noise
    floor by this processing gain value.

    References:

    * [Understand SINAD, ENOB, SNR, THD, THD + N, and SFDR so You Don't Get Lost in the Noise Floor - ADI](https://www.analog.com/media/en/training-seminars/tutorials/MT-003.pdf)

    Args:
        M: Number of FFT bins

    Returns:
        y: FFT processing gain (dB)
    """
    return 10 * np.log(M / 2)

PSD(x, fs, norm=False, max_mag=1.0, fft_shift=False)

Calculates Power Spectral Density (PSD) of a given time signal

Parameters:

Name Type Description Default
x ndarray

Sample data vector (time domain)

required
fs float

Sample frequency of x (Hz)

required
norm bool

When True, normalize max frequency bin (e.x. fundamental) to 0.0 dB

False
max_mag float

maximum input magnitude (or max I or Q value for complex) to calculate dBFS. Only used when norm == False

1.0
fft_shift

Shifts the zero-frequency component to the center of the spectrum

False
Source code in rfproto/measurements.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def PSD(
    x: np.ndarray, fs: float, norm: bool = False, max_mag: float = 1.0, fft_shift=False
):
    """Calculates Power Spectral Density (PSD) of a given time signal

    Args:
        x: Sample data vector (time domain)
        fs: Sample frequency of `x` (Hz)
        norm: When True, normalize max frequency bin (e.x. fundamental) to 0.0 dB
        max_mag: maximum input magnitude (or max I or Q value for complex) to calculate dBFS. Only used when `norm == False`
        fft_shift: Shifts the zero-frequency component to the center of the spectrum
    """
    real = np.isrealobj(x)

    psd = utils.dbfs_fft(x, max_mag if not norm else 1.0)
    if norm:
        psd -= psd.max(axis=0)

    # Real PSD is only 0 -> fs/2
    numFreqBins = len(psd) if not real else 2 * len(psd)

    if fft_shift:
        psd = np.fft.fftshift(psd)
        freqBin = np.linspace(-len(psd) // 2, len(psd) // 2, len(psd)) * (
            fs / numFreqBins
        )
    else:
        freqBin = np.linspace(1, len(psd), len(psd)) * (fs / numFreqBins)

    return freqBin, psd

SFDR(x, fs, norm=False, ignore_percent=0.1, max_mag=1.0)

Spurious free dynamic range (SFDR) is the ratio of the RMS value of the signal to the RMS value of the worst spurious signal regardless of where it falls in the frequency spectrum. The worst spur may or may not be a harmonic of the original signal. SFDR is an important specification in communications systems because it represents the smallest value of signal that can be distinguished from a large interfering signal (blocker). SFDR is specified here w.r.t. an actual signal amplitude (dBc). Thus, it's expected that the given signal vector x has some main frequency component greater than any spurs present in the spectrum to return a sensible value.

References:

Parameters:

Name Type Description Default
x ndarray

Sample data vector (time domain)

required
fs float

Sample frequency of x (Hz)

required
norm bool

When True, normalize max frequency bin (e.x. fundamental) to 0.0 dB

False
ignore_percent float

The fraction of total samples that are ignored around the fundamental for spurs

0.1
max_mag float

maximum input magnitude (or max I or Q value for complex) to calculate dBFS. Only used when norm == False

1.0
Source code in rfproto/measurements.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def SFDR(
    x: np.ndarray,
    fs: float,
    norm: bool = False,
    ignore_percent: float = 0.1,
    max_mag: float = 1.0,
):
    """Spurious free dynamic range (SFDR) is the ratio of the RMS value of the
    signal to the RMS value of the worst spurious signal regardless of where it
    falls in the frequency spectrum. The worst spur may or may not be a
    harmonic of the original signal. SFDR is an important specification in
    communications systems because it represents the smallest value of signal
    that can be distinguished from a large interfering signal (blocker). SFDR
    is specified here w.r.t. an actual signal amplitude (dBc). Thus, it's expected
    that the given signal vector `x` has some main frequency component greater than
    any spurs present in the spectrum to return a sensible value.

    References:

    * [Understand SINAD, ENOB, SNR, THD, THD + N, and SFDR so You Don't Get Lost in the Noise Floor - ADI](https://www.analog.com/media/en/training-seminars/tutorials/MT-003.pdf)
    * [MonsieurV/py-findpeaks](https://github.com/MonsieurV/py-findpeaks)

    Args:
        x: Sample data vector (time domain)
        fs: Sample frequency of `x` (Hz)
        norm: When True, normalize max frequency bin (e.x. fundamental) to 0.0 dB
        ignore_percent: The fraction of total samples that are ignored around the fundamental for spurs
        max_mag: maximum input magnitude (or max I or Q value for complex) to calculate dBFS. Only used when `norm == False`


    """
    # TODO: really use https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html ?
    freqBin, Y = PSD(x, fs, norm, max_mag)
    idx_fc = np.argmax(Y)  # give index of spectrum fundamental (Fc)
    # +/- percentage from Fc to ignore for SFDR calculation so phase noise or
    # leakage from the main tone doesn't affect these calcs (default +/-10%)
    min_idx = int(idx_fc - int(ignore_percent * len(Y)))
    if min_idx < 0:  # limits check
        min_idx = 0
    max_idx = int(idx_fc + int(ignore_percent * len(Y)))
    if max_idx > len(Y) - 1:
        max_idx = len(Y) - 1
    PSD_non_fc = np.copy(Y)
    PSD_non_fc[min_idx:max_idx] = -10000  # null freq bins we want to ignore
    idx_spur = np.argmax(PSD_non_fc)  # index of largest spur
    d = dict()  # use dictionary for multiple, named return values
    d["fc_dB"] = Y[idx_fc]
    d["fc_Hz"] = freqBin[idx_fc]
    d["spur_dB"] = Y[idx_spur]
    d["spur_Hz"] = freqBin[idx_spur]
    d["SFDR"] = Y[idx_fc] - Y[idx_spur]
    return d

ideal_SNR(N)

Calculate the ideal SNR of an \(N\)-bit ADC/DAC

References:

Parameters:

Name Type Description Default
N int

Number of bits

required

Returns:

Name Type Description
y float

SNR (dB)

Source code in rfproto/measurements.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def ideal_SNR(N: int) -> float:
    """Calculate the ideal SNR of an $N$-bit ADC/DAC

    References:

    * [Understand SINAD, ENOB, SNR, THD, THD + N, and SFDR so You Don't Get Lost in the Noise Floor - ADI](https://www.analog.com/media/en/training-seminars/tutorials/MT-003.pdf)

    Args:
        N: Number of bits

    Returns:
        y: SNR (dB)
    """
    return (6.02 * N) + 1.76