Complete VIDEO-001 through VIDEO-004 software video studies
This commit is contained in:
113
tools/video_iq_replay.py
Normal file
113
tools/video_iq_replay.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""VIDEO-001: лёгкий повтор сохранённого IQ через штатный приёмник Lab053.
|
||||
|
||||
Не обращается к SDR. Внутрь RX передаются только последовательные блоки IQ.
|
||||
Ожидаемые SHA читаются лишь после строгого PHY/FEC/CRC и сборки JPEG.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
|
||||
SAMPLE_RATE = 8_000_000
|
||||
BLOCK_SAMPLES = 65_536
|
||||
|
||||
|
||||
def replay(source: Path) -> dict:
|
||||
from experiments.lab051_receiver_performance import ImageSink
|
||||
from experiments.lab053_streaming_receiver import StreamingReceiver
|
||||
|
||||
receiver = StreamingReceiver(workers=1, candidate_capacity=4)
|
||||
startup = dict(receiver.processor.startup)
|
||||
sink = ImageSink()
|
||||
images: list[dict] = []
|
||||
bytes_read = 0
|
||||
start = time.perf_counter()
|
||||
|
||||
def consume(packets) -> None:
|
||||
for packet in packets:
|
||||
item = sink.offer(packet, time.perf_counter() - start)
|
||||
if item is not None:
|
||||
images.append({"image_id": item["image_id"],
|
||||
"sha256": hashlib.sha256(item["jpeg"]).hexdigest(),
|
||||
"dimensions": item["dimensions"]})
|
||||
|
||||
try:
|
||||
with source.open("rb") as stream:
|
||||
while raw := stream.read(BLOCK_SAMPLES * 8):
|
||||
if len(raw) % 8:
|
||||
raise ValueError("IQ содержит неполный complex64 отсчёт")
|
||||
bytes_read += len(raw)
|
||||
consume(receiver.push_iq_block(np.frombuffer(raw, dtype=np.complex64)))
|
||||
consume(receiver.finish())
|
||||
elapsed = time.perf_counter() - start
|
||||
packet_count = receiver.packet_count
|
||||
candidate_count = receiver.candidate_count
|
||||
finally:
|
||||
receiver.close()
|
||||
iq_seconds = bytes_read / 8 / SAMPLE_RATE
|
||||
return {"startup": startup, "iq_bytes": bytes_read, "iq_seconds": iq_seconds,
|
||||
"replay_wall_seconds": elapsed,
|
||||
"replay_wall_over_iq_seconds": elapsed / iq_seconds,
|
||||
"iq_seconds_over_replay_wall": iq_seconds / elapsed,
|
||||
"total_with_jit_warmup_seconds": elapsed + startup["warmup_seconds"],
|
||||
"accepted_packets": packet_count, "strict_candidates": candidate_count,
|
||||
"completed_images": len(images), "images": images}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--numba-packages", required=True, type=Path)
|
||||
parser.add_argument("--fixture-json", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
if not arguments.source.is_file() or not arguments.fixture_json.is_file():
|
||||
parser.error("IQ или зафиксированная ведомость JPEG отсутствует")
|
||||
if not arguments.numba_packages.is_dir():
|
||||
parser.error("Локальные пакеты Numba отсутствуют")
|
||||
if arguments.source.stat().st_size % 8:
|
||||
parser.error("IQ содержит неполный complex64 отсчёт")
|
||||
sys.path.insert(0, str(arguments.numba_packages))
|
||||
fixture = json.loads(arguments.fixture_json.read_text(encoding="utf-8"))
|
||||
expected = {item["image_id"]: item["sha256"] for item in fixture["images"]}
|
||||
with tempfile.TemporaryDirectory(prefix="video001_numba_") as cache:
|
||||
os.environ["NUMBA_CACHE_DIR"] = cache
|
||||
first = replay(arguments.source)
|
||||
repeat = replay(arguments.source)
|
||||
for result in (first, repeat):
|
||||
result["exact_images_after_verdict"] = sum(
|
||||
expected.get(item["image_id"]) == item["sha256"] for item in result["images"]
|
||||
)
|
||||
result["all_expected_images_exact"] = (
|
||||
result["completed_images"] == len(expected)
|
||||
and result["exact_images_after_verdict"] == len(expected)
|
||||
)
|
||||
result.pop("images")
|
||||
document = {"status": "MEASURED_SAVED_IQ_NO_SDR", "source": str(arguments.source),
|
||||
"source_bytes": arguments.source.stat().st_size,
|
||||
"comparison": "Первый и повторный проходы в одном процессе; пустой JIT-кеш в начале",
|
||||
"path_scope": "Время включает чтение файла, RX, сборку и JPEG decode; без графиков, послеблочной истории и сравнения SHA внутри измерения",
|
||||
"first": first, "repeat": repeat, "rf_tx": 0}
|
||||
arguments.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
arguments.output.write_text(json.dumps(document, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print(json.dumps({label: {name: value[name] for name in ("startup", "replay_wall_seconds",
|
||||
"iq_seconds_over_replay_wall", "accepted_packets", "completed_images",
|
||||
"all_expected_images_exact")} for label, value in (("first", first), ("repeat", repeat))},
|
||||
ensure_ascii=False), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user