146 lines
7.2 KiB
Python
146 lines
7.2 KiB
Python
"""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()
|