Complete VIDEO-001 through VIDEO-004 software video studies

This commit is contained in:
LittleSam129
2026-09-16 18:27:35 +03:00
parent af7c05f2da
commit 95f664076a
403 changed files with 392009 additions and 0 deletions

134
tools/video003_stream.py Normal file
View 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()