"""Эталонные векторы и строгий приём программного QPSK VIDEO-003.""" from __future__ import annotations import math import numpy as np import pytest from protocol import punctured_fec from protocol.video003_modem import ( PREAMBLE_SYMBOLS, build_frame, known_preamble_bits, qpsk_soft, qpsk_symbols, receive_frame, rrc_roundtrip, samples_per_symbol, ) def test_gray_mapping_known_vector_and_unit_energy() -> None: bits = np.array([0, 0, 0, 1, 1, 1, 1, 0], dtype=np.uint8) expected = np.array([-1-1j, -1+1j, 1+1j, 1-1j]) / math.sqrt(2) assert np.allclose(qpsk_symbols(bits), expected) assert np.allclose(np.abs(expected) ** 2, 1.0) assert np.array_equal((qpsk_soft(expected) >= 0).astype(np.uint8), bits) pairs = bits.reshape(4, 2) assert all(np.count_nonzero(pairs[index] != pairs[(index + 1) % 4]) == 1 for index in range(4)) def test_preamble_matches_lab043_reference() -> None: from experiments.lab043_pluto_to_rtlsdr import pilot_prbs7 assert np.array_equal(known_preamble_bits(), pilot_prbs7(PREAMBLE_SYMBOLS, 0x7F)) @pytest.mark.parametrize("rate", ["1/2", "2/3", "3/4"]) def test_puncturing_preserves_existing_code_and_decodes_without_noise(rate: str) -> None: random_generator = np.random.default_rng(3303) for size in (1, 13, 256, 1012): bits = random_generator.integers(0, 2, size * 8, dtype=np.uint8) coded = punctured_fec.encode(bits, rate) assert len(coded) == punctured_fec.coded_bit_count(len(bits), rate) restored = punctured_fec.decode_soft(2.0 * coded - 1.0, len(bits), rate) assert np.array_equal(restored, bits) assert punctured_fec.effective_rate(len(bits), rate) < float(rate[0]) / float(rate[2]) def test_puncturing_patterns_and_erasure_locations() -> None: assert punctured_fec.PUNCTURE_PATTERNS == { "1/2": (1, 1), "2/3": (1, 1, 1, 0), "3/4": (1, 1, 1, 0, 0, 1) } values = np.arange(punctured_fec.coded_bit_count(8, "2/3"), dtype=float) + 1 restored = punctured_fec.depuncture_soft(values, 8, "2/3") assert np.array_equal(restored[3::4], np.zeros(len(restored[3::4]))) with pytest.raises(ValueError): punctured_fec.depuncture_soft(values[:-1], 8, "2/3") @pytest.mark.parametrize("modulation,symbol_rate", [ ("BPSK", 500_000), ("QPSK", 250_000), ("QPSK", 500_000) ]) @pytest.mark.parametrize("rate", ["1/2", "2/3", "3/4"]) def test_independent_receive_phase_quadrant_and_cfo( modulation: str, symbol_rate: int, rate: str ) -> None: payload = bytes(range(48)) frame = build_frame(payload, modulation, rate) indexes = np.arange(len(frame.symbols), dtype=float) phase = 0.23 + (math.pi / 2 if modulation == "QPSK" else 0.0) shifted = frame.symbols * np.exp(1j * (phase + 2 * math.pi * 73 * indexes / symbol_rate)) prefix = np.full(9, 0.1 + 0.2j) outcome = receive_frame(np.concatenate((prefix, shifted)), modulation, rate, symbol_rate) assert outcome.preamble_found and outcome.phy_header_pass and outcome.crc_pass assert outcome.start_symbol == 9 assert outcome.payload == payload assert outcome.frequency_offset_hz == pytest.approx(73, abs=0.1) @pytest.mark.parametrize("symbol_rate", [250_000, 500_000]) def test_rrc_known_timing_preserves_qpsk_packet(symbol_rate: int) -> None: frame = build_frame(b"RRC check", "QPSK", "2/3") filtered = rrc_roundtrip(frame.symbols, symbol_rate) result = receive_frame(filtered, "QPSK", "2/3", symbol_rate) assert samples_per_symbol(symbol_rate) == 8_000_000 // symbol_rate assert result.crc_pass and result.payload == b"RRC check" def test_corruption_and_truncation_cannot_become_crc_pass() -> None: frame = build_frame(bytes(range(32)), "QPSK", "3/4") truncated = receive_frame(frame.symbols[:-20], "QPSK", "3/4", 500_000) assert truncated.preamble_found and truncated.phy_header_pass and not truncated.crc_pass broken = frame.symbols.copy() broken[PREAMBLE_SYMBOLS:PREAMBLE_SYMBOLS + 144] *= -1 rejected = receive_frame(broken, "QPSK", "3/4", 500_000) assert rejected.preamble_found and not rejected.phy_header_pass and not rejected.crc_pass def test_full_packet_limit_is_enforced_before_modulation() -> None: assert len(build_frame(bytes(1012), "QPSK", "3/4").packet) == 1024 with pytest.raises(ValueError, match="1024"): build_frame(bytes(1013), "QPSK", "3/4")