import numpy as np from galileo_rx.config import FrontendConfig, RfConfig from galileo_rx.frontend.frontend_chain import FrontendChain from galileo_rx.frontend.mixer import PhaseAccumulatorNco def test_nco_frequency_word_accuracy_and_block_continuity() -> None: full = PhaseAccumulatorNco(10.0e6, 1.234567e6, phase_width=32) blocked = PhaseAccumulatorNco(10.0e6, 1.234567e6, phase_width=32) expected = full.generate(100) actual = np.concatenate([blocked.generate(37), blocked.generate(63)]) np.testing.assert_allclose(actual, expected, atol=1e-14) assert abs(full.actual_frequency_hz - 1.234567e6) <= 10.0e6 / 2**33 def test_real_if_mixer_sign_and_phase_continuity() -> None: fs = 8.0e6 rf = RfConfig(sample_format="float64", input_is_real_if=True, sampling_frequency_hz=fs, intermediate_frequency_hz=2.0e6) chain = FrontendChain(rf, FrontendConfig()) time_s = np.arange(160) / fs real_if = np.cos(2 * np.pi * 2.0e6 * time_s) result = np.concatenate([chain.process(real_if[:61]), chain.process(real_if[61:])]) # A real mixer produces desired DC plus the unfiltered negative-frequency image. assert abs(np.mean(result) - 0.5) < 0.02 def test_signed_sample_conversion() -> None: rf = RfConfig(sample_format="int8", sampling_frequency_hz=4.0e6, intermediate_frequency_hz=0.0, sample_bit_width=8) chain = FrontendChain(rf, FrontendConfig()) result = chain.process(np.array([-128, 0, 127], dtype=np.int8)) np.testing.assert_allclose(result.real, [-1.0, 0.0, 127 / 128])