Files
SDR-Rover/tools/video004_pipeline_benchmark.py

179 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Замер от программных символов до видеокадра с внешним 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()