"""Короткий непрерывный программный поток максимальных пакетов VIDEO-003.""" from __future__ import annotations import argparse import json import math from pathlib import Path import time import numpy as np from protocol import punctured_fec from protocol.video_stream_transport import ( FrameType, VideoFragment, decode_fragment, encode_fragment, ) from protocol.video003_modem import ( PHY_HEADER_SYMBOLS, PREAMBLE_SYMBOLS, build_frame, receive_frame, ) from tools.video003_design import ROOT, estimate DEFAULT_OUTPUT = ROOT / "data/processed/video003/continuous_stream.json" SEED = 20260916 PACKETS = 32 SYMBOL_RATE = 500_000 EBN0_DB = 9.0 CARRIER_OFFSET_HZ = 73.0 TRANSIENT_SYMBOLS = 42 VIDEO_BYTES = 995 PHY_PAYLOAD_BYTES = 1012 def run(packet_count: int = PACKETS, seed: int = SEED) -> dict: if not 1 <= packet_count <= 256: raise ValueError("число пакетов вне проверяемого диапазона") generator = np.random.default_rng(seed) records = [] for rate in ("2/3", "3/4"): frames = [] for index in range(packet_count): data = generator.integers(0, 256, VIDEO_BYTES, dtype=np.uint8).tobytes() fragment = VideoFragment(0x56303033, index, 0, 1, FrameType.KEYFRAME if index % 10 == 0 else FrameType.DELTA_FRAME, 0, data) payload = encode_fragment(fragment, PHY_PAYLOAD_BYTES) if len(payload) != PHY_PAYLOAD_BYTES: raise AssertionError("неполный PHY payload") frames.append(build_frame(payload, "QPSK", rate, sequence_number=index)) symbols = np.concatenate([part for frame in frames for part in (frame.symbols, np.zeros(TRANSIENT_SYMBOLS, dtype=np.complex128))]) body_symbols = math.ceil(punctured_fec.coded_bit_count(1024 * 8, rate) / 2) n0 = (body_symbols / (1024 * 8)) / 10 ** (EBN0_DB / 10) sigma = math.sqrt(n0 / 2) phase = generator.uniform(-math.pi, math.pi) + math.pi / 2 indexes = np.arange(len(symbols), dtype=np.float64) received = symbols * np.exp(1j * (phase + 2 * math.pi * CARRIER_OFFSET_HZ * indexes / SYMBOL_RATE)) received += sigma * (generator.standard_normal(len(symbols)) + 1j * generator.standard_normal(len(symbols))) leading = sigma * (generator.standard_normal(11) + 1j * generator.standard_normal(11)) received = np.concatenate((leading, received)) cursor = 0 outcomes = [] started = time.perf_counter() for index, expected in enumerate(frames): outcome = receive_frame(received[cursor:], "QPSK", rate, SYMBOL_RATE) fragment_pass = False if outcome.crc_pass and outcome.payload is not None: fragment = decode_fragment(outcome.payload, PHY_PAYLOAD_BYTES) fragment_pass = fragment.frame_id == index and len(fragment.data) == VIDEO_BYTES outcomes.append({ "packet_index": index, "preamble_found": outcome.preamble_found, "phy_header_pass": outcome.phy_header_pass, "crc_pass": outcome.crc_pass, "packet_exact": outcome.packet == expected.packet, "video_fragment_pass": fragment_pass, "start_symbol_relative": outcome.start_symbol, }) if not outcome.crc_pass or outcome.start_symbol is None: break # Без заголовка/CRC нельзя знать следующий размер кадра. received_packet_bytes = len(outcome.packet) received_body_symbols = math.ceil( punctured_fec.coded_bit_count(received_packet_bytes * 8, rate) / 2) cursor += (outcome.start_symbol + PREAMBLE_SYMBOLS + PHY_HEADER_SYMBOLS + received_body_symbols + TRANSIENT_SYMBOLS) processing_seconds = time.perf_counter() - started signal_seconds = len(symbols) / SYMBOL_RATE verified_fragments = sum(item["video_fragment_pass"] for item in outcomes) budget = estimate("QPSK", SYMBOL_RATE, rate, PHY_PAYLOAD_BYTES, 0) records.append({ "modulation": "QPSK", "symbol_rate": SYMBOL_RATE, "fec_rate": rate, "phy_payload_bytes": PHY_PAYLOAD_BYTES, "full_packet_bytes": 1024, "video_fragment_bytes": VIDEO_BYTES, "gap_ms": 0, "ebn0_db": EBN0_DB, "n0_complex": n0, "attempted_packets": packet_count, "decoded_packets": len(outcomes), "crc_pass_count": sum(item["crc_pass"] for item in outcomes), "exact_packet_count": sum(item["packet_exact"] for item in outcomes), "verified_video_fragment_count": verified_fragments, "observed_video_bits": verified_fragments * VIDEO_BYTES * 8, "stream_symbols_excluding_leading_noise": len(symbols), "signal_duration_s": signal_seconds, "processing_duration_s": processing_seconds, "processing_to_signal_ratio": processing_seconds / signal_seconds, "offline_verified_video_bps": verified_fragments * VIDEO_BYTES * 8 / signal_seconds, "calculated_zero_loss_video_bps": budget.useful_video_rate_bps, "records": outcomes, "receiver_truth_inputs": "modulation, FEC rate, symbol rate, known preamble and PHY format only", }) return {"seed": seed, "packet_count": packet_count, "energy_condition": "Es=1, Eb=Es*body_symbols/information_bits, includes QPSK padding but excludes fixed PHY overhead", "scenario": "symbol-spaced AWGN, one continuous stream, 42 transient symbols, no inter-packet gap", "results": records, "rf_tx": 0} def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--packet-count", type=int, default=PACKETS) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) args = parser.parse_args() result = run(args.packet_count) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") for item in result["results"]: print(f"QPSK 500 / FEC {item['fec_rate']}: " f"CRC {item['crc_pass_count']}/{item['attempted_packets']}, " f"{item['offline_verified_video_bps'] / 1000:.3f} кбит/с, " f"CPU/signal {item['processing_to_signal_ratio']:.2f}") if __name__ == "__main__": main()