Files
SDR-Rover/tools/video003_stream.py

135 lines
6.7 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.
"""Короткий непрерывный программный поток максимальных пакетов 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()