Complete VIDEO-001 through VIDEO-004 software video studies

This commit is contained in:
LittleSam129
2026-09-16 18:27:35 +03:00
parent af7c05f2da
commit 95f664076a
403 changed files with 392009 additions and 0 deletions

View 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")