diff --git a/.gitignore b/.gitignore index b14692e..cf220ea 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,9 @@ *.log *.tmp pytest_cache/ -__pycache__/ \ No newline at end of file +__pycache__/ +*.egg-info +tests/cocotb_tests/sim_build/ +fft_ddc_performance_report.json +performance_plots/ +tests/cocotb_tests/results.xml diff --git a/fft_ddc_performance_report.json b/fft_ddc_performance_report.json deleted file mode 100644 index 0bdc396..0000000 --- a/fft_ddc_performance_report.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "timestamp": "2026-01-17 13:01:30", - "system_info": { - "numpy_version": "2.4.1", - "python_version": "3.12.12 (main, Nov 7 2025, 00:07:10) [GCC 13.3.0]", - "platform": "linux" - }, - "benchmarks": { - "fft_performance": { - "signal_type": "wideband_noise", - "fft_sizes": [ - 256, - 512, - 1024, - 2048, - 4096, - 8192 - ], - "computation_times": [ - 0.48517668689999593, - 0.3279225473999986, - 0.23730186390000085, - 0.2073529940000128, - 0.1923519671999941, - 0.20967159909999963 - ], - "throughput_msps": [ - 20.61078421532064, - 30.494615509930753, - 42.13772212178551, - 48.21891310621434, - 51.979380016448864, - 47.66616004694752 - ], - "memory_usage": [ - 4096, - 8192, - 16384, - 32768, - 65536, - 131072 - ], - "accuracy_metrics": [ - 0.0, - 1.7901010750429764e-16, - 0.0, - 0.0, - 0.0, - 1.7400193226405283e-16 - ] - }, - "ddc_performance": { - "configurations": [ - { - "sample_rate": 50000000.0, - "center_freq": 95500000.0, - "bandwidth": 200000.0 - }, - { - "sample_rate": 100000000.0, - "center_freq": 95500000.0, - "bandwidth": 200000.0 - }, - { - "sample_rate": 100000000.0, - "center_freq": 1000000000.0, - "bandwidth": 1000000.0 - }, - { - "sample_rate": 50000000.0, - "center_freq": 2400000000.0, - "bandwidth": 5000000.0 - }, - { - "sample_rate": 20000000.0, - "center_freq": 433000000.0, - "bandwidth": 100000.0 - } - ], - "throughput_msps": [ - 5.054311416696046, - 4.641915102659372, - 5.008147329197962, - 5.8903278768785015, - 7.657704821531766 - ], - "processing_times": [ - 0.09892544379998754, - 0.21542832599999429, - 0.1996746369999755, - 0.08488491819999808, - 0.026117486199996164 - ], - "decimation_factors": [ - 125, - 250, - 50, - 5, - 100 - ], - "output_bw_utilization": [ - 0.0068359375, - 0.005859375, - 0.0078125, - 0.091796875, - 0.0068359375 - ], - "filter_performance": [ - { - "processed_samples": 2501024, - "total_processing_time": 0.4955767939999305, - "throughput_samples_per_sec": 5046693.126636496, - "output_sample_rate": 400000.0, - "decimation_factor": 125, - "filter_length": 257, - "bandwidth": 200000.0, - "center_frequency": 95500000.0 - }, - { - "processed_samples": 5001024, - "total_processing_time": 1.0776191990000257, - "throughput_samples_per_sec": 4640808.185897847, - "output_sample_rate": 400000.0, - "decimation_factor": 250, - "filter_length": 257, - "bandwidth": 200000.0, - "center_frequency": 95500000.0 - }, - { - "processed_samples": 5001024, - "total_processing_time": 0.9988457439999934, - "throughput_samples_per_sec": 5006803.132556605, - "output_sample_rate": 2000000.0, - "decimation_factor": 50, - "filter_length": 257, - "bandwidth": 1000000.0, - "center_frequency": 1000000000.0 - }, - { - "processed_samples": 2501024, - "total_processing_time": 0.4236935970000104, - "throughput_samples_per_sec": 5902907.236995461, - "output_sample_rate": 10000000.0, - "decimation_factor": 5, - "filter_length": 101, - "bandwidth": 5000000.0, - "center_frequency": 2400000000.0 - }, - { - "processed_samples": 1001024, - "total_processing_time": 0.12981373699994947, - "throughput_samples_per_sec": 7711233.211015177, - "output_sample_rate": 200000.0, - "decimation_factor": 100, - "filter_length": 257, - "bandwidth": 100000.0, - "center_frequency": 433000000.0 - } - ] - }, - "combined_pipeline": { - "configuration": { - "sample_rate": 100000000.0, - "center_freq": 95500000.0, - "bandwidth": 200000.0, - "fft_size": 2048 - }, - "processing_times": [ - 12.526878776999979, - 12.527599296999995, - 12.556478982999977 - ], - "memory_efficiency": [], - "real_time_factor": [ - 12.526878776999979, - 12.527599296999995, - 12.556478982999977 - ], - "average_throughput_msps": 7.976398993127074, - "std_processing_time": 0.013786981007740947 - } - } -} \ No newline at end of file diff --git a/performance_plots/ddc_performance.png b/performance_plots/ddc_performance.png deleted file mode 100644 index 9df8c66..0000000 Binary files a/performance_plots/ddc_performance.png and /dev/null differ diff --git a/performance_plots/fft_performance.png b/performance_plots/fft_performance.png deleted file mode 100644 index 145b489..0000000 Binary files a/performance_plots/fft_performance.png and /dev/null differ diff --git a/tests/cocotb_tests/Makefile b/tests/cocotb_tests/Makefile new file mode 100644 index 0000000..5f5e47f --- /dev/null +++ b/tests/cocotb_tests/Makefile @@ -0,0 +1,17 @@ +# Makefile for cocotb tests + +# Defaults +SIM ?= icarus +TOPLEVEL_LANG ?= verilog + +# Point to the Verilog source files +VERILOG_SOURCES = $(shell find ../../verilog -name "*.v") + +# TOPLEVEL is the name of the toplevel module in your Verilog design +TOPLEVEL ?= cic_decimator + +# MODULE is the basename of the Python test file +MODULE ?= test_cic_decimator + +# Include cocotb's make rules +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tests/cocotb_tests/sim_build/cmds.f b/tests/cocotb_tests/sim_build/cmds.f new file mode 100644 index 0000000..3e26e00 --- /dev/null +++ b/tests/cocotb_tests/sim_build/cmds.f @@ -0,0 +1 @@ ++timescale+1ns/1ps diff --git a/tests/cocotb_tests/test_cic_decimator.py b/tests/cocotb_tests/test_cic_decimator.py index e6dde5e..79aab74 100644 --- a/tests/cocotb_tests/test_cic_decimator.py +++ b/tests/cocotb_tests/test_cic_decimator.py @@ -1,85 +1,71 @@ -# test_cic_decimator.py - cocotb testbench for CIC decimator validation import cocotb +from cocotb.triggers import Timer, RisingEdge from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, FallingEdge -from cocotb.result import TestFailure import numpy as np - -@cocotb.test() -async def test_cic_decimator_basic(dut): - """Basic CIC decimator functionality test""" - # Start 100 MHz system clock - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - # Reset sequence +async def reset_dut(dut): dut.rst_n.value = 0 - await Timer(100, units="ns") + await Timer(10, units='ns') dut.rst_n.value = 1 - await RisingEdge(dut.clk) - - # Test impulse response (theoretical gain = decimation^stages = 8^3 = 512) - expected_gain = 512 - - # Send impulse - dut.data_in.value = 0x10000 # Impulse - dut.data_valid.value = 1 - await RisingEdge(dut.clk) - dut.data_valid.value = 0 - - # Feed 7 zeros (total 8 samples for decimation by 8) - for i in range(7): - dut.data_in.value = 0 - dut.data_valid.value = 1 - await RisingEdge(dut.clk) - dut.data_valid.value = 0 - - # Wait for output (pipeline delay) - await Timer(1000, units="ns") - - if dut.output_valid.value: - output_val = int(dut.data_out.value) - expected_output = (0x10000 << 21) >> 9 # Scaled accordingly per decimation - cocotb.log.info(f"CIC output: {output_val:08X}, expected range around {expected_output:08X}") - else: - raise TestFailure("CIC decimator did not produce valid output") - - cocotb.log.info("CIC decimator basic test PASSED") - + await Timer(10, units='ns') @cocotb.test() -async def test_cic_decimator_saturation(dut): - """Test CIC decimator saturation handling""" +async def test_cic_decimator(dut): + # Clock clock = Clock(dut.clk, 10, units="ns") cocotb.start_soon(clock.start()) # Reset - dut.rst_n.value = 0 - await Timer(100, units="ns") - dut.rst_n.value = 1 - await RisingEdge(dut.clk) - - # Test with maximum input - dut.data_in.value = 0x7FFFFFFF # Maximum positive - dut.data_valid.value = 1 - await RisingEdge(dut.clk) - dut.data_valid.value = 0 - - # Feed zeros - for i in range(7): - dut.data_in.value = 0 + await reset_dut(dut) + + # Test parameters + input_width = dut.INPUT_WIDTH.value + stages = dut.STAGES.value + decimation = dut.DECIMATION.value + + # Generate input signal (a simple ramp) + test_data = np.arange(0, 256, 1, dtype=np.int32) + + # Golden model + integrator = np.zeros(stages, dtype=np.int64) + comb = np.zeros(stages, dtype=np.int64) + comb_delay = np.zeros(stages, dtype=np.int64) + expected_output = [] + + for i in range(len(test_data)): + # Integrator stage + integrator[0] += test_data[i] + for j in range(1, stages): + integrator[j] += integrator[j-1] + + # Decimator and Comb stage + if (i + 1) % decimation == 0: + temp_in = integrator[stages-1] + comb[0] = temp_in - comb_delay[0] + comb_delay[0] = temp_in + for j in range(1, stages): + temp_in = comb[j-1] + comb[j] = temp_in - comb_delay[j] + comb_delay[j] = temp_in + + # Gain compensation + gain = decimation ** stages + output = comb[stages-1] // gain + expected_output.append(output) + + # Drive DUT + output_from_dut = [] + for data in test_data: + dut.data_in.value = int(data) dut.data_valid.value = 1 await RisingEdge(dut.clk) - dut.data_valid.value = 0 + if dut.output_valid.value == 1: + output_from_dut.append(dut.data_out.value.signed_integer) - # Wait for result - await Timer(1000, units="ns") + dut.data_valid.value = 0 - if dut.output_valid.value: - output_val = int(dut.data_out.value) - assert abs(output_val) <= 0x7FFFFFFF, f"CIC output saturated incorrectly: {output_val:08X}" - else: - raise TestFailure("CIC decimator saturation test failed - no output") + # Compare results + assert len(output_from_dut) == len(expected_output), f"Output length mismatch: DUT={len(output_from_dut)}, Expected={len(expected_output)}" + for i in range(len(expected_output)): + assert output_from_dut[i] == expected_output[i], f"Mismatch at index {i}: DUT={output_from_dut[i]}, Expected={expected_output[i]}" - cocotb.log.info("CIC decimator saturation test PASSED") diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py new file mode 100644 index 0000000..5efb2dc --- /dev/null +++ b/tests/test_edge_cases.py @@ -0,0 +1,35 @@ +import pytest +import numpy as np +import sys +import os + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'wideband-sdr-software')) +from digital_downconverter import DigitalDownconverter + +def test_empty_input_signal(): + ddc = DigitalDownconverter(1e6, 2e5, 1e5) + empty_signal = np.array([], dtype=np.complex64) + result = ddc.apply_ddc(empty_signal) + assert result.size == 0 + +def test_invalid_input_type(): + ddc = DigitalDownconverter(1e6, 2e5, 1e5) + with pytest.raises(TypeError): + ddc.apply_ddc("not a numpy array") + +def test_zero_bandwidth(): + with pytest.raises(ValueError): + DigitalDownconverter(1e6, 2e5, 0) + +def test_large_decimation_factor(): + # This might not raise an error, but it's good to check behavior + ddc = DigitalDownconverter(1e6, 2e5, 100) # Very narrow bandwidth -> large decimation + signal = np.random.randn(10000) + 1j * np.random.randn(10000) + result = ddc.apply_ddc(signal) + assert result.size > 0 + +def test_mismatched_types(): + ddc = DigitalDownconverter(1e6, 2e5, 1e5) + signal = np.random.randn(1024).astype(np.float32) # Real signal + with pytest.raises(TypeError): + ddc.apply_ddc(signal) diff --git a/wideband-sdr-software/digital_downconverter.py b/wideband-sdr-software/digital_downconverter.py index e2deeb7..5805420 100644 --- a/wideband-sdr-software/digital_downconverter.py +++ b/wideband-sdr-software/digital_downconverter.py @@ -34,6 +34,8 @@ def __init__(self, sample_rate: float, center_freq: float, bandwidth: float): center_freq: Center frequency to downconvert from in Hz bandwidth: Output bandwidth in Hz """ + if bandwidth <= 0: + raise ValueError("Bandwidth must be positive") self.sample_rate = sample_rate self.center_freq = center_freq self.bandwidth = bandwidth @@ -111,6 +113,9 @@ def apply_ddc(self, samples: np.ndarray, """ start_time = time.perf_counter() + if not isinstance(samples, np.ndarray) or not np.iscomplexobj(samples): + raise TypeError("Input samples must be a complex numpy array") + if len(samples) == 0: return np.array([], dtype=np.complex64)