Complete VIDEO-001 through VIDEO-004 software video studies
This commit is contained in:
134
tools/video003_stream.py
Normal file
134
tools/video003_stream.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Короткий непрерывный программный поток максимальных пакетов 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()
|
||||
Reference in New Issue
Block a user