61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
"""Формирование комплексного 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))
|