"""Формирование комплексного IQ для программного видеотракта VIDEO-005.""" from __future__ import annotations from functools import lru_cache from typing import Sequence import numpy as np from scipy.signal import upfirdn from protocol.bpsk_radio import root_raised_cosine_taps from protocol.video003_modem import ( RRC_ROLLOFF, RRC_SPAN_SYMBOLS, SAMPLE_RATE_HZ, OfflineFrame, samples_per_symbol, ) SYMBOL_RATE = 500_000 NOMINAL_CARRIER_HZ = 125_000.0 GUARD_SAMPLES = 64 @lru_cache(maxsize=2) def rrc_taps(symbol_rate: int = SYMBOL_RATE) -> np.ndarray: """Возвращает неизменяемые коэффициенты уже применяемого RRC-фильтра.""" taps = root_raised_cosine_taps( RRC_ROLLOFF, samples_per_symbol(symbol_rate), RRC_SPAN_SYMBOLS ) taps.flags.writeable = False return taps def shape_packet( frame: OfflineFrame, symbol_rate: int = SYMBOL_RATE, carrier_hz: float = NOMINAL_CARRIER_HZ, ) -> np.ndarray: """Формирует один IQ-пакет с защитными паузами, без канальных нарушений.""" if not np.isfinite(carrier_hz) or abs(carrier_hz) >= SAMPLE_RATE_HZ / 2: raise ValueError("несущая должна лежать внутри полосы дискретизации") if frame.modulation != "QPSK" or frame.rate != "2/3": raise ValueError("VIDEO-005 использует QPSK и внутренний FEC 2/3") shaped = upfirdn(rrc_taps(symbol_rate), frame.symbols, up=samples_per_symbol(symbol_rate)) envelope = np.pad(shaped, (GUARD_SAMPLES, GUARD_SAMPLES)) phase = 2 * np.pi * carrier_hz * np.arange(envelope.size) / SAMPLE_RATE_HZ return (envelope * np.exp(1j * phase)).astype(np.complex64) def shape_stream( frames: Sequence[OfflineFrame], symbol_rate: int = SYMBOL_RATE, carrier_hz: float = NOMINAL_CARRIER_HZ, ) -> np.ndarray: """Соединяет IQ-пакеты; приёмнику их границы не передаются.""" if not frames: raise ValueError("пустой поток") return np.concatenate(tuple(shape_packet(frame, symbol_rate, carrier_hz) for frame in frames))