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