diff --git a/.coverage b/.coverage deleted file mode 100644 index 4d3eb89..0000000 Binary files a/.coverage and /dev/null differ diff --git a/.gitignore b/.gitignore index b1d9456..cfec992 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ python_adc_eval.egg-info dist .ruff_cache +.coverage diff --git a/adc_eval/spectrum.py b/adc_eval/spectrum.py index 6f1b97d..3150961 100644 --- a/adc_eval/spectrum.py +++ b/adc_eval/spectrum.py @@ -118,7 +118,9 @@ def calc_psd(data, fs, nfft=2**12, single_sided=False): psd = np.mean(XF, axis=1) / (fs / nfft) # average the ffts and divide by bin width freq = fs * np.linspace(0, 1, nfft) if single_sided: + # First we double all the bins, then we halve the DC bin psd = 2 * psd[0 : int(nfft / 2)] + psd[0] /= 2 freq = freq[0 : int(nfft / 2)] return (freq, psd) diff --git a/tests/test_calc_psd.py b/tests/test_calc_psd.py new file mode 100644 index 0000000..8aae2e8 --- /dev/null +++ b/tests/test_calc_psd.py @@ -0,0 +1,139 @@ +"""Test the calc_psd method.""" + +import unittest +import numpy as np +from unittest import mock +from adc_eval import spectrum + + +class TestCalcPSD(unittest.TestCase): + """Test the calc_psd method.""" + + def setUp(self): + """Initialize tests.""" + self.nfft = 2**8 + self.nlen = 2**18 + accuracy = 0.01 + self.bounds = [1 - accuracy, 1 + accuracy] + np.random.seed(1) + + def test_calc_psd_randomized_dual(self): + """Test calc_psd with random data.""" + for i in range(0, 10): + data = np.random.randn(self.nlen) + (freq, psd) = spectrum.calc_psd(data, 1, nfft=self.nfft, single_sided=False) + mean_val = np.mean(psd) + self.assertTrue(self.bounds[0] <= mean_val <= self.bounds[1], msg=mean_val) + + def test_calc_psd_randomized_single(self): + """Test calc_psd with random data and single-sided.""" + for i in range(0, 10): + data = np.random.randn(self.nlen) + (freq, psd) = spectrum.calc_psd(data, 1, nfft=self.nfft, single_sided=True) + mean_val = np.mean(psd) + self.assertTrue( + 2 * self.bounds[0] <= mean_val <= 2 * self.bounds[1], msg=mean_val + ) + + def test_calc_psd_zeros_dual(self): + """Test calc_psd with zeros.""" + data = np.zeros(self.nlen) + (freq, psd) = spectrum.calc_psd(data, 1, nfft=self.nfft, single_sided=False) + mean_val = np.mean(psd) + self.assertTrue( + self.bounds[0] - 1 <= mean_val <= self.bounds[1] - 1, msg=mean_val + ) + + def test_calc_psd_zeros_single(self): + """Test calc_psd with zeros and single-sided..""" + data = np.zeros(self.nlen) + (freq, psd) = spectrum.calc_psd(data, 1, nfft=self.nfft, single_sided=True) + mean_val = np.mean(psd) + self.assertTrue( + self.bounds[0] - 1 <= mean_val <= self.bounds[1] - 1, msg=mean_val + ) + + def test_calc_psd_ones_dual(self): + """Test calc_psd with ones.""" + data = np.ones(self.nlen) + (freq, psd) = spectrum.calc_psd(data, 1, nfft=self.nfft, single_sided=False) + mean_val = np.mean(psd) + self.assertTrue(self.bounds[0] <= mean_val <= self.bounds[1], msg=mean_val) + + def test_calc_psd_ones_single(self): + """Test calc_psd with ones and single-sided.""" + data = np.ones(self.nlen) + (freq, psd) = spectrum.calc_psd(data, 1, nfft=self.nfft, single_sided=True) + mean_val = np.mean(psd) + self.assertTrue( + 2 * self.bounds[0] <= mean_val <= 2 * self.bounds[1], msg=mean_val + ) + + def test_calc_psd_two_sine_dual(self): + """Test calc_psd with two sine waves.""" + fs = 1 + fbin = fs / self.nfft + f1 = 29 * fbin + f2 = 97 * fbin + a1 = 0.37 + a2 = 0.11 + t = 1 / fs * np.linspace(0, self.nlen - 1, self.nlen) + data = a1 * np.sin(2 * np.pi * f1 * t) + a2 * np.sin(2 * np.pi * f2 * t) + (freq, psd) = spectrum.calc_psd(data, fs, nfft=self.nfft, single_sided=False) + exp_peaks = [ + round(a1**2 / 4 * self.nfft, 3), + round(a2**2 / 4 * self.nfft, 3), + ] + exp_f1 = [round(f1, 2), round(fs - f1, 2)] + exp_f2 = [round(f2, 2), round(fs - f2, 2)] + + peak1 = max(psd) + ipeaks = np.where(psd >= peak1 * self.bounds[0])[0] + fpeaks = [round(freq[ipeaks[0]], 2), round(freq[ipeaks[1]], 2)] + + self.assertEqual(round(peak1, 3), exp_peaks[0]) + self.assertListEqual(fpeaks, exp_f1) + + psd[ipeaks[0]] = 0 + psd[ipeaks[1]] = 0 + + peak2 = max(psd) + ipeaks = np.where(psd >= peak2 * self.bounds[0])[0] + fpeaks = [round(freq[ipeaks[0]], 2), round(freq[ipeaks[1]], 2)] + + self.assertEqual(round(peak2, 3), exp_peaks[1]) + self.assertListEqual(fpeaks, exp_f2) + + def test_calc_psd_two_sine_single(self): + """Test calc_psd with two sine waves, single-eided.""" + fs = 1 + fbin = fs / self.nfft + f1 = 29 * fbin + f2 = 97 * fbin + a1 = 0.37 + a2 = 0.11 + t = 1 / fs * np.linspace(0, self.nlen - 1, self.nlen) + data = a1 * np.sin(2 * np.pi * f1 * t) + a2 * np.sin(2 * np.pi * f2 * t) + (freq, psd) = spectrum.calc_psd(data, fs, nfft=self.nfft, single_sided=True) + exp_peaks = [ + round(a1**2 / 2 * self.nfft, 3), + round(a2**2 / 2 * self.nfft, 3), + ] + exp_f1 = round(f1, 2) + exp_f2 = round(f2, 2) + + peak1 = max(psd) + ipeak = np.where(psd == peak1)[0][0] + fpeak = round(freq[ipeak], 2) + + self.assertEqual(round(peak1, 3), exp_peaks[0]) + self.assertEqual(fpeak, exp_f1) + + psd[ipeak] = 0 + + peak2 = max(psd) + ipeak = np.where(psd == peak2)[0][0] + fpeak = round(freq[ipeak], 2) + + self.assertEqual(round(peak2, 3), exp_peaks[1]) + self.assertEqual(fpeak, exp_f2) diff --git a/tests/test_spectrum.py b/tests/test_spectrum.py index af1495c..03cfd51 100644 --- a/tests/test_spectrum.py +++ b/tests/test_spectrum.py @@ -9,6 +9,10 @@ class TestSpectrum(unittest.TestCase): """Test the spectrum module.""" + def setUp(self): + """Initialize tests.""" + pass + def test_db_to_pow_places(self): """Test the db_to_pow conversion with multiple places.""" test_val = 29.9460497 @@ -43,30 +47,6 @@ def test_enob(self): for i in range(0, len(exp_val)): self.assertEqual(spectrum.enob(test_val, places=i), exp_val[i]) - def test_calc_psd_two_sided(self): - """Test calc_psd with dummy input.""" - sq2 = np.sqrt(2) / 4 - data = np.array([0, sq2, 0.5, sq2, 0, -sq2, -0.5, -sq2]) - exp_psd = np.array([0, 0.5, 0, 0, 0, 0, 0, 0.5]) - exp_freq = np.array([i / (len(data) - 1) for i in range(0, len(data))]) - (freq, psd) = spectrum.calc_psd(data, 1, nfft=8, single_sided=False) - - for index in range(0, len(psd)): - self.assertEqual(round(psd[index], 5), round(exp_psd[index], 5)) - self.assertEqual(round(freq[index], 5), round(exp_freq[index], 5)) - - def test_calc_psd_one_sided(self): - """Test calc_psd with dummy input.""" - sq2 = np.sqrt(2) / 4 - data = np.array([0, sq2, 0.5, sq2, 0, -sq2, -0.5, -sq2]) - exp_psd = 2 * np.array([0, 0.5, 0, 0]) - exp_freq = np.array([i / (len(data) - 1) for i in range(0, len(data))]) - (freq, psd) = spectrum.calc_psd(data, 1, nfft=8, single_sided=True) - - for index in range(0, len(psd)): - self.assertEqual(round(psd[index], 5), round(exp_psd[index], 5)) - self.assertEqual(round(freq[index], 5), round(exp_freq[index], 5)) - @mock.patch("adc_eval.spectrum.calc_psd") def test_get_spectrum(self, mock_calc_psd): """Test that the get_spectrum method returns power spectrum.""" @@ -80,3 +60,47 @@ def test_get_spectrum(self, mock_calc_psd): self.assertEqual( spectrum.get_spectrum(None, fs=fs, nfft=nfft), (None, exp_spectrum) ) + + def test_sndr_sfdr_outputs(self): + """Test the sndr_sfdr method outputs.""" + data = np.array([1, 2, 91, 7]) + freq = np.array([100, 200, 300, 400]) + full_scale = -3 + nfft = 2**8 + exp_return = { + "sig": { + "freq": 300, + "bin": 2, + "power": 91, + "dB": 19.6, + "dBFS": round(19.590 - full_scale, 1), + }, + "spur": { + "freq": 400, + "bin": 3, + "power": 7, + "dB": 8.5, + "dBFS": round(8.451 - full_scale, 1), + }, + "noise": { + "floor": 18 / nfft, + "power": 9, + "rms": 3, + "dBHz": round(-11.5297 - full_scale, 1), + }, + "sndr": { + "dBc": 10.0, + "dBFS": round(full_scale - 9.542, 1), + }, + "sfdr": { + "dBc": 11.1, + "dBFS": round(full_scale - 8.451, 1), + }, + "enob": { + "bits": round((full_scale - 11.3024) / 6.02, 1), + }, + } + + result = spectrum.sndr_sfdr(data, freq, nfft, 0, full_scale=full_scale) + for key, val in exp_return.items(): + self.assertDictEqual(result[key], val, msg=key)