"""Детерминированный контроль IQ-приёмника и полного программного видео.""" from __future__ import annotations import argparse import cProfile import hashlib import json from pathlib import Path import pstats import statistics import sys import time import numpy as np from protocol.packet import parse_packet from protocol.video003_modem import build_frame from protocol.video005_iq_waveform import SAMPLE_RATE_HZ, shape_stream from protocol.video005_receiver import IqStreamResult, receive_iq_stream from tools.video005_channel import impair, lab066_like from tools.video005_video import build_fixture, run_case ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "data/processed/video005/benchmark.json" SEED = 20260917 def _sha(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _strict_outcomes(result: IqStreamResult, expected: list[bytes]) -> dict: observed = {parse_packet(item.packet).sequence_number: _sha(item.packet) for item in result.packets} reference = {index: _sha(packet) for index, packet in enumerate(expected)} return { "crc_valid_packets": len(result.packets), "exact_packets": sum(observed.get(index) == digest for index, digest in reference.items()), "unexpected_or_wrong_packets": sum(index not in reference or digest != reference[index] for index, digest in observed.items()), "packet_sha256_by_sequence": observed, } def _profile(receiver_iq) -> dict: profiler = cProfile.Profile() profiler.enable() receive_iq_stream(receiver_iq) profiler.disable() stats = pstats.Stats(profiler).strip_dirs().sort_stats("cumtime") raw_stats = vars(stats)["stats"] # Атрибут pstats создаётся динамически. listing = [{"function": key[2], "file": key[0], "calls": values[1], "self_s": values[2], "cumulative_s": values[3]} for key, values in raw_stats.items()] listing.sort(key=lambda item: item["cumulative_s"], reverse=True) selected = ("oaconvolve", "search_preamble_grid", "refine_timing", "estimate_from_preamble", "track_qpsk_body", "qpsk_soft", "depuncture_soft", "decode_soft", "_trellis", "parse_packet") stage_functions = [item for item in listing if item["function"] in selected] return {"profile_total_s": vars(stats)["total_tt"], "top_functions": listing[:40], "selected_stages": stage_functions} def build_radio_fixture() -> tuple[list[bytes], np.ndarray, np.ndarray]: """Канонический IQ-вход для 32 полных пакетов; эталон остаётся снаружи RX.""" frames = [build_frame(bytes(((bit + index) * 37) % 256 for bit in range(1012)), "QPSK", "2/3", sequence_number=index) for index in range(32)] clean_iq = shape_stream(frames) channel_iq = impair(clean_iq, cfo_hz=20, phase_degrees=15, timing_offset_symbols=0.1, ebn0_db=9, seed=SEED) return [frame.packet for frame in frames], clean_iq, channel_iq def run() -> dict: reference, clean_iq, channel_iq = build_radio_fixture() duration = len(channel_iq) / SAMPLE_RATE_HZ signal_sha256 = _sha(channel_iq.tobytes()) started = time.perf_counter() first = receive_iq_stream(channel_iq) first_time = time.perf_counter() - started first_outcome = _strict_outcomes(first, reference) warm_times = [] warm_results = [] for _ in range(3): started = time.perf_counter() received = receive_iq_stream(channel_iq) warm_times.append(time.perf_counter() - started) warm_results.append(received) if _strict_outcomes(received, reference) != first_outcome: raise AssertionError("вход IQ дал разные строгие пакеты на повторах") median_time = statistics.median(warm_times) profile = _profile(channel_iq) awgn_frames = [build_frame(bytes(((bit + index) * 37) % 256 for bit in range(1012)), "QPSK", "2/3", sequence_number=index) for index in range(16)] awgn_iq = shape_stream(awgn_frames) awgn = [] for ebn0_db in (0, 3, 6, 9): received = receive_iq_stream(impair(awgn_iq, ebn0_db=ebn0_db, seed=SEED)) awgn.append({"ebn0_db": ebn0_db, **_strict_outcomes(received, [frame.packet for frame in awgn_frames])}) diagnostic_iq, diagnostic_parameters = lab066_like(clean_iq, seed=SEED) diagnostic = receive_iq_stream(diagnostic_iq) diagnostic_outcome = _strict_outcomes(diagnostic, reference) fixture = build_fixture(20) video_cases = [run_case(fixture, scenario) for scenario in ( "clean", "random_1", "random_5", "keyframe_double", "keyframe_triple", "corrupt_packet", )] if not all(case["exact_crc_packets"] for case in video_cases): raise AssertionError("после CRC обнаружен неправильный пакет") if not all(not case["video"]["wrong_encoded_frames"] and not case["video"]["wrong_decoded_frames"] and not case["video"]["old_frame_replay"] for case in video_cases): raise AssertionError("в видеотракте обнаружены испорченные или повторные кадры") clean_video = video_cases[0] return { "status": ( "PARTIAL" if (clean_video["end_to_end_realtime_factor"] > 0.67 or video_cases[2]["video"]["frame_loss_rate"] > 0) else "PASS" ), "seed": SEED, "rf_tx": 0, "sdr_hardware": "NOT ACCESSED", "radio": { "packets": len(reference), "iq_samples": len(channel_iq), "iq_duration_s": duration, "signal_sha256": signal_sha256, "jit_first_run_s": first_time, "jit_first_outcome": first_outcome, "warm_times_s": warm_times, "warm_median_s": median_time, "realtime_factor": median_time / duration, "realtime_speed": duration / median_time, "strict_outcome": first_outcome, "stage_wall_s": warm_results[-1].stage_time_s, "cprofile": profile, }, "awgn": awgn, "lab066_like": {"parameters": diagnostic_parameters, "outcome": diagnostic_outcome}, "video": { "source_sha256": fixture.source_sha256, "original_frames": len(fixture.frames), "scheduled_phy_packets": len(fixture.transmissions), "iq_duration_s": len(fixture.iq) / SAMPLE_RATE_HZ, "preparation_time_s": fixture.preparation_time_s, "cases": video_cases, }, "sample_clock_offset": "NOT MODELED IN VIDEO-005", } 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.parent.mkdir(parents=True, exist_ok=True) OUTPUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(f"RADIO IQ factor={result['radio']['realtime_factor']:.3f}; " f"exact={result['radio']['strict_outcome']['exact_packets']}/32") for case in result["video"]["cases"]: print(f"{case['scenario']}: CRC={case['crc_valid_radio_packets']}, " f"displayed={case['video']['unique_decoded_frames']}/20, " f"radio={case['radio_realtime_factor']:.3f}, " f"end-to-end={case['end_to_end_realtime_factor']:.3f}") if __name__ == "__main__": main()