Complete VIDEO-001 through VIDEO-004 software video studies
This commit is contained in:
105
tests/test_video003_modem.py
Normal file
105
tests/test_video003_modem.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Эталонные векторы и строгий приём программного 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")
|
||||
Reference in New Issue
Block a user