Skip to content

Commit 28571b3

Browse files
committed
Core module tests completed
1 parent 47fdca0 commit 28571b3

3 files changed

Lines changed: 251 additions & 33 deletions

File tree

adc_eval/eval/spectrum.py

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ def get_spectrum(data, fs=1, nfft=2**12, single_sided=True):
3838

3939
def window_data(data, window="rectangular"):
4040
"""Applies a window to the time-domain data."""
41-
wsize = data.size
41+
try:
42+
wsize = data.size
43+
except AttributeError:
44+
data = np.array(data)
45+
wsize = data.size
46+
4247
windows = {
4348
"rectangular": (np.ones(wsize), 1.0),
4449
"hanning": (np.hanning(wsize), 1.633)
@@ -64,26 +69,10 @@ def plot_spectrum(
6469
no_plot=False,
6570
yaxis="power",
6671
single_sided=True,
67-
fscale="MHz",
72+
fscale=("MHz", 1e6),
6873
):
6974
"""Plot Power Spectrum for input signal."""
70-
71-
fscalar = {
72-
"uHz": 1e-6,
73-
"mHz": 1e-3,
74-
"Hz": 1,
75-
"kHz": 1e3,
76-
"MHz": 1e6,
77-
"GHz": 1e9,
78-
"THz": 1e12,
79-
}
80-
if fscale not in fscalar:
81-
print(f"WARNING: {fscale} not implemented. Defaulting to 'MHz'.")
82-
fscale = "MHz"
83-
84-
# Window the data and get the single or dual-sided spectrum
85-
wdata = window_data(data, window=window)
86-
(freq, pwr) = get_spectrum(wdata, fs=fs, nfft=nfft, single_sided=single_sided)
75+
(freq, pwr) = get_spectrum(data, fs=fs, nfft=nfft, single_sided=single_sided)
8776

8877
# Calculate the fullscale range of the spectrum in Watts
8978
full_scale = calc.dBW(dr**2 / 8)
@@ -99,13 +88,7 @@ def plot_spectrum(
9988
lut_key = yaxis.lower()
10089
scalar = yaxis_lut[lut_key][0]
10190
yunits = yaxis_lut[lut_key][1]
102-
try:
103-
xscale = fscalar[fscale]
104-
except KeyError:
105-
print(
106-
f"WARNING: {fscale} not a valid option for fscale. Valid inputs are {fscalar.keys()}."
107-
)
108-
print(" Defaulting to Hz.")
91+
xscale = fscale[1]
10992

11093
# Convert to dBW and perform scalar based on y-axis scaling input
11194
psd_out = calc.dBW(pwr, places=3) - scalar
@@ -119,9 +102,7 @@ def plot_spectrum(
119102
psd_ss = pwr
120103
if not single_sided:
121104
# Get single-sided spectrum for SNDR and Harmonic stats
122-
(f_ss, psd_ss) = get_spectrum(
123-
data * windows[window] * wscale, fs=fs, nfft=nfft, single_sided=True
124-
)
105+
(f_ss, psd_ss) = get_spectrum(data, fs=fs, nfft=nfft, single_sided=True)
125106

126107
sndr_stats = calc.sndr_sfdr(psd_ss, f_ss, fs, nfft, leak=leak, full_scale=full_scale)
127108
harm_stats = calc.find_harmonics(
@@ -143,7 +124,7 @@ def plot_spectrum(
143124

144125
# If plotting, prep plot and generate all required axis strings
145126
if not no_plot:
146-
plt_str = calc.get_plot_string(stats, full_scale, fs, nfft, window, xscale, fscale)
127+
plt_str = calc.get_plot_string(stats, full_scale, fs, nfft, window, xscale, fscale[0])
147128
fig, ax = plt.subplots(figsize=(15, 8))
148129
ax.plot(freq / xscale, psd_out)
149130
ax.set_ylabel(f"Power Spectrum ({yunits})", fontsize=18)
@@ -226,8 +207,24 @@ def analyze(
226207
fscale="MHz",
227208
):
228209
"""Perform spectral analysis on input waveform."""
210+
fscalar = {
211+
"uHz": 1e-6,
212+
"mHz": 1e-3,
213+
"Hz": 1,
214+
"kHz": 1e3,
215+
"MHz": 1e6,
216+
"GHz": 1e9,
217+
"THz": 1e12,
218+
}
219+
if fscale not in fscalar:
220+
print(f"WARNING: {fscale} not implemented. Defaulting to 'MHz'.")
221+
fscale = "MHz"
222+
223+
# Window the data
224+
wdata = window_data(data, window=window)
225+
229226
(freq, spectrum, stats) = plot_spectrum(
230-
data,
227+
wdata,
231228
fs=fs,
232229
nfft=nfft,
233230
dr=dr,
@@ -237,7 +234,7 @@ def analyze(
237234
no_plot=no_plot,
238235
yaxis=yaxis,
239236
single_sided=single_sided,
240-
fscale=fscale,
237+
fscale=(fscale, fscalar[fscale]),
241238
)
242239

243240
return (freq, spectrum, stats)

tests/eval/test_spectrum.py

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
RTOL = 0.05
1010
NLEN = 2**18
11-
NFFT = 2**8
11+
NFFT = 2**10
1212
DATA_SINE = [
1313
{
1414
"f1": np.random.randint(1, NFFT / 4 - 1),
@@ -175,3 +175,155 @@ def test_calc_psd_two_sine_single(data):
175175

176176
assert np.allclose(peak2, exp_peaks[1], rtol=RTOL), assertmsg
177177
assert np.allclose(fpeak, exp_f2), assertmsg
178+
179+
180+
def test_window_data_as_list():
181+
"""Tests the window_data function when given a list instead of numpy array."""
182+
data = np.random.rand(NLEN).tolist()
183+
wdata = spectrum.window_data(data, window="rectangular")
184+
185+
assert type(data) == type(list())
186+
assert type(wdata) == type(np.ndarray([]))
187+
188+
189+
def test_window_data_bad_window_type(capfd):
190+
"""Tests the window_data function with an incorrect window selection."""
191+
data = np.random.rand(NLEN)
192+
wdata = spectrum.window_data(data, window="foobar")
193+
captured = capfd.readouterr()
194+
195+
assert data.size == wdata.size
196+
assert data.all() == wdata.all()
197+
assert "WARNING" in captured.out
198+
199+
200+
@mock.patch("adc_eval.eval.spectrum.plot_spectrum")
201+
def test_analyze_bad_input_scalar(mock_plot_spectrum, capfd):
202+
"""Tests bad input scalar keys."""
203+
mock_plot_spectrum.return_value = (None, None, None)
204+
mock_plot_spectrum.side_effect = lambda *args, **kwargs: (kwargs, None, None)
205+
(kwargs, _, _) = spectrum.analyze([0], 1, fscale="foobar")
206+
captured = capfd.readouterr()
207+
208+
assert "WARNING" in captured.out
209+
assert kwargs.get("fscale") == ("MHz", 1e6)
210+
211+
212+
@mock.patch("adc_eval.eval.spectrum.plot_spectrum")
213+
def test_analyze_valid_input_scalar(mock_plot_spectrum):
214+
"""Tests the valid input scalar keys."""
215+
mock_plot_spectrum.return_value = (None, None, None)
216+
mock_plot_spectrum.side_effect = lambda *args, **kwargs: (kwargs, None, None)
217+
218+
test_vals = {
219+
"Hz": 1,
220+
"kHz": 1e3,
221+
"MHz": 1e6,
222+
"GHz": 1e9,
223+
}
224+
for key, val in test_vals.items():
225+
(kwargs, _, _) = spectrum.analyze([0], 1, fscale=key)
226+
assert kwargs.get("fscale") == (key, val)
227+
228+
229+
@mock.patch("adc_eval.eval.calc.sndr_sfdr")
230+
@mock.patch("adc_eval.eval.calc.find_harmonics")
231+
def test_analyze_no_plot(mock_sndr_sfdr, mock_find_harmonics):
232+
"""Tests the psd output of the analyze function with no plotting."""
233+
data = np.random.rand(NLEN)
234+
data_sndr = {
235+
"sig": {"bin": 1, "power": 2},
236+
}
237+
data_harms = {"harmonics": 3}
238+
exp_stats = {**data_sndr, **data_harms}
239+
240+
mock_sndr_sfdr.return_value = data_sndr
241+
mock_find_harmonics = data_harms
242+
243+
(freq, psd, stats) = spectrum.analyze(
244+
data,
245+
fs=1,
246+
nfft=NFFT,
247+
dr=1,
248+
harmonics=0,
249+
leak=0,
250+
window="rectangular",
251+
no_plot=True,
252+
yaxis="power",
253+
single_sided=True,
254+
fscale="Hz",
255+
)
256+
257+
assert freq.all() == np.linspace(0, 1, int(NFFT/2)).all()
258+
assert psd.size == int(NFFT/2)
259+
260+
for key, value in stats.items():
261+
assert value == exp_stats[key]
262+
263+
264+
@mock.patch("adc_eval.eval.calc.sndr_sfdr")
265+
@mock.patch("adc_eval.eval.calc.find_harmonics")
266+
def test_analyze_no_plot_dual(mock_sndr_sfdr, mock_find_harmonics):
267+
"""Tests the psd output of the analyze function with no plotting."""
268+
data = np.random.rand(NLEN)
269+
data_sndr = {
270+
"sig": {"bin": 1, "power": 2},
271+
}
272+
data_harms = {"harmonics": 3}
273+
exp_stats = {**data_sndr, **data_harms}
274+
275+
mock_sndr_sfdr.return_value = data_sndr
276+
mock_find_harmonics = data_harms
277+
278+
(freq, psd, stats) = spectrum.analyze(
279+
data,
280+
fs=1,
281+
nfft=NFFT,
282+
dr=1,
283+
harmonics=0,
284+
leak=0,
285+
window="rectangular",
286+
no_plot=True,
287+
yaxis="power",
288+
single_sided=False,
289+
fscale="Hz",
290+
)
291+
292+
assert freq.all() == np.linspace(-0.5, 0.5, NFFT-1).all()
293+
assert psd.size == NFFT
294+
for key, value in stats.items():
295+
assert value == exp_stats[key]
296+
297+
298+
@mock.patch("adc_eval.eval.calc.sndr_sfdr")
299+
@mock.patch("adc_eval.eval.calc.find_harmonics")
300+
def test_analyze_no_plot_magnitude(mock_sndr_sfdr, mock_find_harmonics):
301+
"""Tests the psd output of the analyze function with no plotting."""
302+
data = np.random.rand(NLEN)
303+
data_sndr = {
304+
"sig": {"bin": 1, "power": 2},
305+
}
306+
data_harms = {"harmonics": 3}
307+
exp_stats = {**data_sndr, **data_harms}
308+
309+
mock_sndr_sfdr.return_value = data_sndr
310+
mock_find_harmonics = data_harms
311+
312+
(freq, psd, stats) = spectrum.analyze(
313+
data,
314+
fs=1,
315+
nfft=NFFT,
316+
dr=1,
317+
harmonics=0,
318+
leak=0,
319+
window="rectangular",
320+
no_plot=True,
321+
yaxis="magnitude",
322+
single_sided=True,
323+
fscale="Hz",
324+
)
325+
326+
assert freq.all() == np.linspace(0, 1, int(NFFT/2)).all()
327+
assert psd.size == int(NFFT/2)
328+
for key, value in stats.items():
329+
assert value == exp_stats[key]

tests/test_signals.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Test the signals module."""
2+
3+
import pytest
4+
import numpy as np
5+
from scipy import stats
6+
from adc_eval import signals
7+
8+
9+
RTOL = 0.01
10+
11+
@pytest.mark.parametrize("nlen", np.random.randint(4, 2**16, 3))
12+
@pytest.mark.parametrize("fs", np.random.uniform(1, 1e9, 3))
13+
def test_time(nlen, fs):
14+
"""Test time with random data."""
15+
value = signals.time(nlen, fs=fs)
16+
assert value.size == nlen
17+
assert value[0] == 0
18+
assert np.isclose(value[nlen-1], (nlen-1)/fs, rtol=RTOL)
19+
20+
21+
@pytest.mark.parametrize("nlen", np.random.randint(2**10, 2**16, 3))
22+
@pytest.mark.parametrize("offset", np.random.uniform(-10, 10, 3))
23+
@pytest.mark.parametrize("amp", np.random.uniform(0, 10, 3))
24+
def test_sin(nlen, offset, amp):
25+
"""Test sine generation with random data."""
26+
fs = np.random.uniform(1, 1e9)
27+
fin = np.random.uniform(fs/10, fs/3)
28+
29+
t = signals.time(nlen, fs=fs)
30+
value = signals.sin(t, amp=amp, offset=offset, freq=fin)
31+
32+
exp_peaks = [offset - amp, amp + offset]
33+
34+
assert value.size == nlen
35+
assert np.isclose(max(value), exp_peaks[1], rtol=RTOL)
36+
assert np.isclose(min(value), exp_peaks[0], rtol=RTOL)
37+
assert value[0] == offset
38+
39+
40+
@pytest.mark.parametrize("nlen", np.random.randint(1, 2**16, 5))
41+
def test_noise_length(nlen):
42+
"""Test noise generation with random data."""
43+
t = signals.time(nlen, fs=1)
44+
value = signals.noise(t, mean=0, std=1)
45+
46+
# Just check correct size
47+
assert value.size == nlen
48+
49+
50+
@pytest.mark.parametrize("std", np.random.uniform(0, 1, 4))
51+
def test_noise_length(std):
52+
"""Test noise is gaussian with random data."""
53+
nlen = 2**12
54+
t = signals.time(nlen, fs=1)
55+
noise = signals.noise(t, mean=0, std=std)
56+
autocorr = np.correlate(noise, noise, mode="full")
57+
autocorr /= max(autocorr)
58+
asize = autocorr.size
59+
60+
midlag = autocorr.size // 2
61+
acorr_nopeak = np.concatenate([autocorr[0:midlag-1], autocorr[midlag+1:]])
62+
63+
shapiro = stats.shapiro(acorr_nopeak)
64+
65+
# Check that middle lag is 1
66+
assert autocorr[midlag] == 1
67+
68+
# Now check that noise is gaussian
69+
assert shapiro.pvalue < 0.01

0 commit comments

Comments
 (0)