Lab053: achieve real-time 5 fps software video receiver

This commit is contained in:
LittleSam129
2026-09-09 10:26:55 +03:00
parent cc38900cae
commit 0368c6d3e9
44 changed files with 472121 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
"""Lab053: сравнение независимых решений Lab052/053 после полного приёма."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import sys
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import numpy as np
from experiments import lab052_candidate_detection as baseline
from experiments.lab053_receiver_profile import SOURCE, write_result
from experiments.lab053_streaming_receiver import StreamingReceiver
class VerdictObserver:
def __init__(self):
self.decisions = []
self.frames = {}
def scores(self, *args):
pass
def outcome(self, base, candidate, outcome):
self.decisions.append((base + candidate.sample_offset, outcome.reason,
base + outcome.refined_start_sample, outcome.accepted))
if outcome.accepted:
frame = outcome.frame
self.frames[base + outcome.refined_start_sample] = {
"end": base + outcome.next_sample, "timing": frame.timing_offset_samples,
"cfo_hz": frame.estimated_cfo_hz, "phase_rad": frame.estimated_phase_rad,
"polarity": frame.bpsk_polarity, "crc": outcome.crc,
"packet_bytes": frame.reception.packet_bytes,
"soft": frame.post_preamble_soft.copy()}
def read_all(receiver, source=SOURCE):
packets = []
try:
with source.open("rb") as capture:
while raw := capture.read(receiver.block_samples * 8):
packets.extend(receiver.push_iq_block(np.frombuffer(raw, np.complex64)))
packets.extend(receiver.finish())
return packets
finally:
if hasattr(receiver, "close"):
receiver.close()
def equivalence():
old = VerdictObserver()
read_all(baseline.StreamingReceiver(observer=old))
results = []
for workers in (1, 2, 4):
new = VerdictObserver()
receiver = StreamingReceiver(workers=workers, observer=new)
read_all(receiver)
same_starts = set(old.frames) == set(new.frames)
rows = []
for start in sorted(set(old.frames) & set(new.frames)):
first, second = old.frames[start], new.frames[start]
rows.append({"start_sample": start,
**{f"same_{key}": first[key] == second[key] for key in ("end", "timing", "polarity", "crc", "packet_bytes")},
"cfo_difference_hz": abs(first["cfo_hz"] - second["cfo_hz"]),
"phase_difference_rad": abs(first["phase_rad"] - second["phase_rad"]),
"maximum_soft_difference": float(np.max(np.abs(first["soft"] - second["soft"]))),
"packet_sha256": hashlib.sha256(second["packet_bytes"]).hexdigest()})
checks = {"same_240_starts": same_starts and len(rows) == 240,
"same_candidate_policy_sequence": old.decisions == new.decisions,
"same_phy_timing_crc_packet_bits": all(all(row[f"same_{key}"] for key in
("end", "timing", "polarity", "crc", "packet_bytes")) for row in rows),
"cfo_within_002_hz": all(row["cfo_difference_hz"] < .002 for row in rows),
"phase_within_2e5_rad": all(row["phase_difference_rad"] < 2e-5 for row in rows),
"soft_within_1e4": all(row["maximum_soft_difference"] < 1e-4 for row in rows)}
result = {"workers": workers, "checks": checks, "passed": all(checks.values()),
"strict_candidates": receiver.candidate_count, "accepted": receiver.packet_count,
"maximum_cfo_difference_hz": max(row["cfo_difference_hz"] for row in rows),
"maximum_phase_difference_rad": max(row["phase_difference_rad"] for row in rows),
"maximum_soft_difference": max(row["maximum_soft_difference"] for row in rows), "packets": rows}
results.append(result)
print(json.dumps({key: value for key, value in result.items() if key != "packets"}), flush=True)
write_result("equivalence.json", {"baseline": "committed Lab052", "workers": results,
"passed": all(row["passed"] for row in results), "oracle_used_in_receiver": False, "rf_tx": 0})
def sensitivity():
physical = baseline.phy.lab045
payload = bytes(range(256))
plan = physical.build_transmission_plan(payload)
raw = baseline.phy.build_parameterized_tx_cs8(plan, baseline.phy.PhyMode(500000))
components = np.frombuffer(raw, np.int8).astype(np.float32)
clean = components[0::2] + 1j * components[1::2]
energy = float(np.sum(np.abs(clean) ** 2)) / plan.frame_symbol_count
rows = []
for ebn0 in (0, 1, 2, 3, 7):
for cfo in (-100, 10, 100):
for seed, scale in ((52031, .002), (52032, .2), (52033, 1.)):
random_generator = np.random.default_rng(seed)
sigma = np.sqrt(energy / (2 * 10 ** (ebn0 / 10)))
phase = .73 + (seed % 2) * np.pi
values = clean * np.exp(1j * (phase + 2*np.pi*cfo*np.arange(len(clean))/8000000))
values += sigma * (random_generator.normal(size=len(clean)) + 1j*random_generator.normal(size=len(clean)))
samples = np.concatenate((np.zeros(63217, np.complex64), (values * scale).astype(np.complex64),
np.zeros(350000, np.complex64)))
verdicts = []
for constructor in (baseline.StreamingReceiver, StreamingReceiver):
receiver = constructor()
packets = []
try:
for start in range(0, len(samples), receiver.block_samples):
packets.extend(receiver.push_iq_block(samples[start:start + receiver.block_samples]))
packets.extend(receiver.finish())
finally:
if hasattr(receiver, "close"):
receiver.close()
verdicts.append(len(packets) == 1 and packets[0].payload == payload)
rows.append({"ebn0_db": ebn0, "cfo_hz": cfo, "seed": seed, "scale": scale,
"baseline_correct": verdicts[0], "optimized_correct": verdicts[1]})
result = {"count": len(rows), "baseline_correct": sum(row["baseline_correct"] for row in rows),
"optimized_correct": sum(row["optimized_correct"] for row in rows),
"lost_baseline_packets": sum(row["baseline_correct"] and not row["optimized_correct"] for row in rows),
"same_verdicts": all(row["baseline_correct"] == row["optimized_correct"] for row in rows),
"cases": rows, "rf_tx": 0}
write_result("sensitivity.json", result)
print(json.dumps({key: value for key, value in result.items() if key != "cases"}), flush=True)
def run_functional_tests() -> None:
assert VerdictObserver().decisions == []
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--stage", choices=("equivalence", "sensitivity"), default="equivalence")
arguments = parser.parse_args()
{"equivalence": equivalence, "sensitivity": sensitivity}[arguments.stage]()
if __name__ == "__main__":
main()