"""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()