"""Tests for the PM modulator 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 PM_PEAK_DEVIATION_RAD, SAMPLE_RATE_BASEBAND pytestmark = pytest.mark.skipif(not HAS_GNURADIO, reason="GNU Radio not installed") class TestPMMod: """Test PM modulation with synthetic signals.""" def test_zero_input_constant_envelope(self): """Zero input should produce exp(j*0) = 1+0j (unit carrier).""" from apollo.pm_mod import pm_mod tb = gr.top_block() n_samples = 10000 data = [0.0] * n_samples src = blocks.vector_source_f(data) mod = pm_mod(pm_deviation=PM_PEAK_DEVIATION_RAD, sample_rate=SAMPLE_RATE_BASEBAND) snk = blocks.vector_sink_c() tb.connect(src, mod, snk) tb.run() output = np.array(snk.data()) assert len(output) == n_samples # Magnitude should be 1.0 (constant envelope) magnitudes = np.abs(output) np.testing.assert_allclose(magnitudes, 1.0, atol=1e-6) # Phase should be 0 (no modulation) phases = np.angle(output) np.testing.assert_allclose(phases, 0.0, atol=1e-6) def test_sine_input_phase_deviation(self): """Sine wave input should produce phase swinging +/- pm_deviation.""" from apollo.pm_mod import pm_mod tb = gr.top_block() n_samples = 100000 sample_rate = SAMPLE_RATE_BASEBAND # Unit-amplitude sine at 10 kHz as modulating signal t = np.arange(n_samples, dtype=np.float64) / sample_rate tone_freq = 10000.0 modulating = np.sin(2 * np.pi * tone_freq * t).astype(np.float32) src = blocks.vector_source_f(modulating.tolist()) mod = pm_mod(pm_deviation=PM_PEAK_DEVIATION_RAD, sample_rate=sample_rate) snk = blocks.vector_sink_c() tb.connect(src, mod, snk) tb.run() output = np.array(snk.data()) phases = np.angle(output) # Peak phase should be approximately pm_deviation peak_phase = np.max(np.abs(phases)) assert peak_phase == pytest.approx(PM_PEAK_DEVIATION_RAD, abs=0.01), ( f"Peak phase {peak_phase} doesn't match deviation {PM_PEAK_DEVIATION_RAD}" ) def test_constant_envelope(self): """PM output should always have |s(t)| = 1.0 regardless of input.""" from apollo.pm_mod import pm_mod tb = gr.top_block() n_samples = 50000 sample_rate = SAMPLE_RATE_BASEBAND # Arbitrary varying input: sum of two tones t = np.arange(n_samples, dtype=np.float64) / sample_rate modulating = ( 0.7 * np.sin(2 * np.pi * 5000 * t) + 0.3 * np.cos(2 * np.pi * 20000 * t) ).astype(np.float32) src = blocks.vector_source_f(modulating.tolist()) mod = pm_mod(pm_deviation=PM_PEAK_DEVIATION_RAD, sample_rate=sample_rate) snk = blocks.vector_sink_c() tb.connect(src, mod, snk) tb.run() output = np.array(snk.data()) magnitudes = np.abs(output) np.testing.assert_allclose(magnitudes, 1.0, atol=1e-6) def test_custom_deviation(self): """Custom pm_deviation should scale output phase accordingly.""" from apollo.pm_mod import pm_mod tb = gr.top_block() n_samples = 100000 sample_rate = SAMPLE_RATE_BASEBAND custom_dev = 0.5 # Unit-amplitude sine t = np.arange(n_samples, dtype=np.float64) / sample_rate tone_freq = 10000.0 modulating = np.sin(2 * np.pi * tone_freq * t).astype(np.float32) src = blocks.vector_source_f(modulating.tolist()) mod = pm_mod(pm_deviation=custom_dev, sample_rate=sample_rate) snk = blocks.vector_sink_c() tb.connect(src, mod, snk) tb.run() output = np.array(snk.data()) phases = np.angle(output) peak_phase = np.max(np.abs(phases)) assert peak_phase == pytest.approx(custom_dev, abs=0.02), ( f"Peak phase {peak_phase} doesn't match custom deviation {custom_dev}" ) def test_block_instantiation(self): """Block should instantiate with default parameters.""" from apollo.pm_mod import pm_mod mod = pm_mod() assert mod is not None assert mod.get_pm_deviation() == PM_PEAK_DEVIATION_RAD def test_set_pm_deviation(self): """Runtime deviation update should take effect.""" from apollo.pm_mod import pm_mod mod = pm_mod() assert mod.get_pm_deviation() == PM_PEAK_DEVIATION_RAD mod.set_pm_deviation(0.25) assert mod.get_pm_deviation() == 0.25