"""Tests for the BPSK demodulator block.""" import numpy as np import pytest try: from gnuradio import blocks, gr HAS_GNURADIO = True except ImportError: HAS_GNURADIO = False from apollo.constants import PCM_HIGH_BIT_RATE, SAMPLE_RATE_BASEBAND pytestmark = pytest.mark.skipif(not HAS_GNURADIO, reason="GNU Radio not installed") class TestBPSKDemod: """Test BPSK demodulation with synthetic signals.""" def test_clean_bpsk_recovery(self): """Known BPSK signal should recover original bits.""" from apollo.bpsk_demod import bpsk_demod tb = gr.top_block() sample_rate = SAMPLE_RATE_BASEBAND symbol_rate = PCM_HIGH_BIT_RATE sps = sample_rate / symbol_rate n_bits = 200 # Generate known bit pattern np.random.seed(99) bits = np.random.randint(0, 2, n_bits) nrz = 2.0 * bits - 1.0 # map 0→-1, 1→+1 # Upsample to sample rate (rectangular pulse shaping) samples_per_bit = int(sps) baseband = np.repeat(nrz, samples_per_bit).astype(np.complex64) src = blocks.vector_source_c(baseband.tolist()) demod = bpsk_demod( symbol_rate=symbol_rate, sample_rate=sample_rate, loop_bw=0.045, ) snk = blocks.vector_sink_b() tb.connect(src, demod, snk) tb.run() output = np.array(snk.data()) # Allow for sync-up time; check that most bits match after settling if len(output) > 50: # Find the best alignment between output and expected bits best_match = 0 for offset in range(min(50, len(output))): end = min(len(output) - offset, len(bits)) matches = np.sum(output[offset : offset + end] == bits[:end]) best_match = max(best_match, matches / end if end > 0 else 0) assert best_match > 0.7, f"Bit recovery rate too low: {best_match:.2%}" def test_block_instantiation(self): from apollo.bpsk_demod import bpsk_demod demod = bpsk_demod() assert demod is not None