"""Замер от программных символов до видеокадра с внешним FEC, без SDR.""" from __future__ import annotations import argparse import cProfile from dataclasses import replace import hashlib import json import math from pathlib import Path import pstats import sys import time import tracemalloc import numpy as np from protocol import punctured_fec from protocol.video_stream_transport import FrameReassembler, FrameType, VideoFragment from protocol.video003_modem import PHY_HEADER_SYMBOLS, PREAMBLE_SYMBOLS, build_frame, receive_frame from protocol.video004_erasure import MAX_VIDEO_BYTES, VideoErasureCollector, encode_block from tools.video004_benchmark import RESULT_ROOT SIGNAL = RESULT_ROOT / "pipeline_symbols.npy" MANIFEST = RESULT_ROOT / "pipeline_manifest.json" OUTPUT = RESULT_ROOT / "pipeline_benchmark.json" PACKETS = 40 SYMBOL_RATE = 500_000 RATE = "2/3" TRANSIENT = 42 def _sha(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def create_fixture() -> None: generator = np.random.default_rng(4004) frames = [] all_wires = [] for block_id in range(4): payload = generator.integers(0, 256, MAX_VIDEO_BYTES * 8, dtype=np.uint8).tobytes() frames.append(payload) sources = tuple((VideoFragment(0x56303434, block_id, index, 8, FrameType.KEYFRAME if block_id == 0 else FrameType.DELTA_FRAME, 0, payload[index * MAX_VIDEO_BYTES:(index + 1) * MAX_VIDEO_BYTES]), math.floor(block_id * 152.3)) for index in range(8)) all_wires.extend(encode_block(sources, block_id, 2)) phy_frames = [build_frame(wire, "QPSK", RATE, sequence_number=index) for index, wire in enumerate(all_wires)] symbols = np.concatenate([part for frame in phy_frames for part in (frame.symbols, np.zeros(TRANSIENT, dtype=np.complex128))]) n0 = (math.ceil(punctured_fec.coded_bit_count(1024 * 8, RATE) / 2) / (1024 * 8) / 10 ** (9 / 10)) sigma = math.sqrt(n0 / 2) indexes = np.arange(len(symbols), dtype=np.float64) received = symbols * np.exp(1j * (0.4 + 2 * math.pi * 73 * indexes / SYMBOL_RATE)) received += sigma * (generator.standard_normal(len(symbols)) + 1j * generator.standard_normal(len(symbols))) received = np.concatenate((sigma * (generator.standard_normal(11) + 1j * generator.standard_normal(11)), received)) np.save(SIGNAL, received, allow_pickle=False) MANIFEST.write_text(json.dumps({ "packet_count": PACKETS, "signal_duration_s": len(symbols) / SYMBOL_RATE, "signal_sha256": _sha(received.tobytes()), "expected_packet_sha256": [_sha(frame.packet) for frame in phy_frames], "expected_frame_sha256": [_sha(frame) for frame in frames], "erased_source_symbol_indices": [0, 10, 20, 30], "rf_tx": 0, }, ensure_ascii=False, indent=2), encoding="utf-8") def decode(received: np.ndarray, backend: str) -> dict: fec_decoder = None if backend == "numba": from protocol.video004_viterbi import decode_soft as fec_decoder elif backend != "legacy": raise ValueError("неизвестный декодер") collector = VideoErasureCollector(0x56303434) reassembler = FrameReassembler(0x56303434, 1012, max_age_ms=300) cursor = 0 packets = [] video = {} for packet_index in range(PACKETS): kwargs = {} if fec_decoder is None else {"fec_decoder": fec_decoder} item = receive_frame(received[cursor:], "QPSK", RATE, SYMBOL_RATE, **kwargs) if not item.crc_pass or item.packet is None or item.payload is None or item.start_symbol is None: raise AssertionError(f"PHY/CRC отказ пакета {packet_index}") packets.append(_sha(item.packet)) body_count = math.ceil(punctured_fec.coded_bit_count(len(item.packet) * 8, RATE) / 2) end_symbol = cursor + item.start_symbol + PREAMBLE_SYMBOLS + PHY_HEADER_SYMBOLS + body_count now_ms = math.ceil((end_symbol - 11) * 1000 / SYMBOL_RATE) cursor = end_symbol + TRANSIENT if packet_index in (0, 10, 20, 30): continue # Контролируемое стирание только после успешного CRC. for delivery in collector.offer(item.payload, now_ms): age = now_ms - delivery.capture_ms if age >= 300: raise AssertionError("просроченный фрагмент прошёл FEC") complete = reassembler.offer_fragment(replace(delivery.fragment, age_ms=age), now_ms) if complete is not None: video[complete.frame_id] = _sha(complete.data) collector.finish() return {"packet_sha256": packets, "frame_sha256": [video[index] for index in sorted(video)], "parity_recoveries": collector.parity_recoveries, "unrecoverable_blocks": collector.unrecoverable_blocks} def run(repetitions: int = 3) -> dict: if not SIGNAL.exists(): create_fixture() received = np.load(SIGNAL, allow_pickle=False) manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) if _sha(received.tobytes()) != manifest["signal_sha256"]: raise AssertionError("сигнал полного доступного пути изменился") measured = {} expected = None for backend in ("legacy", "numba"): samples = [] for _ in range(repetitions): started = time.perf_counter() output = decode(received, backend) samples.append(time.perf_counter() - started) if expected is not None and output != expected: raise AssertionError("выход полного доступного пути изменился") expected = output samples.sort() measured[backend] = {"wall_times_s": samples, "median_s": samples[len(samples) // 2], "realtime_factor": samples[len(samples) // 2] / manifest["signal_duration_s"]} if (expected["packet_sha256"] != manifest["expected_packet_sha256"] or expected["frame_sha256"] != manifest["expected_frame_sha256"] or expected["parity_recoveries"] != 4 or expected["unrecoverable_blocks"]): raise AssertionError("полный доступный путь не восстановил кадры точно") profile = cProfile.Profile() profile.enable() decode(received, "numba") profile.disable() stats = pstats.Stats(profile).strip_dirs().sort_stats("cumtime") listing = [{"function": key[2], "file": key[0], "calls": value[1], "self_s": value[2], "cumulative_s": value[3]} for key, value in stats.stats.items()] listing.sort(key=lambda item: item["cumulative_s"], reverse=True) tracemalloc.start() decode(received, "numba") _, peak = tracemalloc.get_traced_memory() tracemalloc.stop() return {"packet_count": PACKETS, "signal_sha256": manifest["signal_sha256"], "signal_duration_s": manifest["signal_duration_s"], "legacy": measured["legacy"], "numba": measured["numba"], "speedup": measured["legacy"]["median_s"] / measured["numba"]["median_s"], "exact_packets_and_frames": True, "parity_recoveries": 4, "profile_total_s": stats.total_tt, "profile_top": listing[:70], "tracemalloc_python_peak_bytes": peak, "timing_recovery": "NOT_IMPLEMENTED_IN_THIS_BENCHMARK", "carrier_tracking_from_raw_iq": "NOT_IMPLEMENTED_IN_THIS_BENCHMARK", "h264_decode": "NOT_INCLUDED; separately tested in transport model", "rf_tx": 0} def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--numba-packages", type=Path) args = parser.parse_args() if args.numba_packages is not None: if not args.numba_packages.is_dir(): parser.error("каталог Numba отсутствует") sys.path.insert(0, str(args.numba_packages.resolve())) result = run() OUTPUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(f"40 packets + erasure FEC: legacy={result['legacy']['realtime_factor']:.3f} " f"numba={result['numba']['realtime_factor']:.3f} recovered={result['parity_recoveries']}") if __name__ == "__main__": main()