Lab051: accelerate 500 ksymbol/s streaming receiver

This commit is contained in:
LittleSam129
2026-09-08 16:04:09 +03:00
parent 60691a09f1
commit 9f33607d3a
13 changed files with 9695 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
"""Регрессии вычислительной архитектуры Lab051, без файлов/оборудования."""
from __future__ import annotations
from collections import Counter
from dataclasses import replace
import numpy as np
import pytest
from scipy.signal import resample_poly
from experiments import lab051_streaming_receiver as lab
from experiments.lab050_streaming_receiver import StreamingReceiver as PreviousReceiver
from experiments.lab050_streaming_receiver import streaming_mode
@pytest.mark.parametrize("seed", range(8))
def test_all_timing_offsets_preserved_including_negative_start(seed):
random_generator = np.random.default_rng(seed)
samples = (random_generator.normal(size=23616) + 1j * random_generator.normal(size=23616)).astype(np.complex64)
with streaming_mode(lab.phy.PhyMode(500000)):
expected = lab.iq.radio.bpsk_modulate(lab.lab045.lab044.preamble_bits())
if seed % 2:
samples[416 + 47:416 + 47 + len(expected)*16:16] += expected * 3
old = lab.ORIGINAL_TIMING(samples, 416, expected)
assert lab.fast_preamble_timing(samples, 416, expected) == old
def test_zero_timing_tie_is_same_first_offset():
with streaming_mode(lab.phy.PhyMode(500000)):
expected = lab.iq.radio.bpsk_modulate(lab.lab045.lab044.preamble_bits())
assert lab.fast_preamble_timing(np.zeros(23616, np.complex64), 416, expected) == -800
@pytest.mark.parametrize("value", [1, 1j, 1+1j, 44])
def test_equal_nonzero_timing_scores_preserve_original_tie(value):
with streaming_mode(lab.phy.PhyMode(500000)):
expected = lab.iq.radio.bpsk_modulate(lab.lab045.lab044.preamble_bits())
samples = np.full(23616, value, np.complex64)
assert lab.fast_preamble_timing(samples, 416, expected) == lab.ORIGINAL_TIMING(samples,416,expected)
@pytest.mark.parametrize("value", [1, 1j, 44])
def test_refinement_constant_iq_preserves_zero_after_local_dc(value):
with streaming_mode(lab.phy.PhyMode(500000)):
samples=np.full(100000,value,np.complex64)
front=lab.BlockFrontEnd(samples,65536,Counter())
window=samples[1000:28456]
assert front.refine(window,front.reference,2000)==lab.ORIGINAL_REFINE(window,front.reference,2000)==0
def test_refinement_equal_nonzero_peaks_preserve_first_maximum():
with streaming_mode(lab.phy.PhyMode(500000)):
samples=np.zeros(100000,np.complex64)
samples[1200]=samples[2200]=1
front=lab.BlockFrontEnd(samples,65536,Counter())
reference=np.zeros(front.reference_length,np.complex64)
reference[0],reference[1]=1,-1
front.reference=reference
window=samples[1000:28456]
assert front.refine(window,reference,2000)==lab.ORIGINAL_REFINE(window,reference,2000)==199
@pytest.mark.parametrize("seed", range(5))
def test_shared_matched_preserves_local_dc_and_both_zero_padded_edges(seed):
random_generator = np.random.default_rng(seed)
values = (random_generator.normal(size=110000) + 1j * random_generator.normal(size=110000) + 4 + 3j).astype(np.complex64)
with streaming_mode(lab.phy.PhyMode(500000)):
front = lab.BlockFrontEnd(values, 70000, Counter())
segment = values[5000 + seed:5000 + seed + 23456]
expected, nominal = lab.ORIGINAL_MATCHED(segment, 0, len(segment))
actual, actual_nominal = front.matched_window(segment, 0, len(segment))
assert actual_nominal == nominal
np.testing.assert_allclose(actual, expected, atol=2e-5, rtol=2e-5)
def test_reused_decimation_is_bit_exact_for_moving_and_truncated_windows():
random_generator = np.random.default_rng(51001)
values = (random_generator.normal(size=1800000) + 1j * random_generator.normal(size=1800000)).astype(np.complex64)
cache = lab.DecimationCache(8)
for base in range(0, len(values) - 10000, 65536):
window = values[base:base + 400123]
expected = resample_poly(window, 1, 8, window=("kaiser", 8))
np.testing.assert_array_equal(cache.get(window, base), expected)
assert cache.values.size <= 50016
assert cache.reused_samples > 1000000
@pytest.mark.parametrize("seed,count", [(0, 1), (1, 32), (2, 244*8), (3, 268*8), (4, 1024*8)])
def test_viterbi_exact_bits_for_noisy_metrics_and_ties(seed, count):
random_generator = np.random.default_rng(seed)
for values in (np.zeros(lab.lab045.conv.encoded_bit_count(count)),
random_generator.normal(size=lab.lab045.conv.encoded_bit_count(count)),
random_generator.choice([-1., 1.], size=lab.lab045.conv.encoded_bit_count(count))):
np.testing.assert_array_equal(lab.fast_viterbi(values, count), lab.ORIGINAL_VITERBI(values, count))
@pytest.mark.parametrize("values,count", [(np.array([np.nan, 0.]), 1), (np.zeros((2,2)), 1),
(np.zeros(12), 1), (np.zeros(12), 0), (np.zeros(12), 1.5)])
def test_viterbi_rejects_same_invalid_input(values, count):
with pytest.raises((ValueError, TypeError)):
lab.fast_viterbi(values, count)
def signal_with_unknown_offsets(seed=51):
random_generator = np.random.default_rng(seed)
plan = lab.lab045.build_transmission_plan(bytes(range(256)))
values = lab.phy.build_parameterized_tx_complex(plan, lab.phy.PhyMode(500000)).astype(np.complex64)
values *= np.exp(1j * (0.73 + 2*np.pi*10*np.arange(len(values))/8000000)).astype(np.complex64)
values += (random_generator.normal(size=len(values)) + 1j*random_generator.normal(size=len(values))).astype(np.complex64)
return np.concatenate((np.zeros(63217,np.complex64), values, np.zeros(450000,np.complex64)))
@pytest.mark.parametrize("block", [32749, 65536])
def test_independent_stream_packets_equal_old_decoder_across_block_boundaries(block):
samples = signal_with_unknown_offsets()
results = []
for receiver in (PreviousReceiver(500000), lab.StreamingReceiver()):
output = []
for start in range(0,len(samples),block):
output.extend(receiver.push_iq_block(samples[start:start + block]))
assert [item.payload for item in output] == [bytes(range(256))]
output.extend(receiver.finish())
results.append([(item.start_sample,item.end_sample,item.payload) for item in output])
assert results[0] == results[1]
def test_invalid_push_and_finish_preserve_bounded_state():
receiver = lab.StreamingReceiver()
for invalid in (np.array([complex(float("nan"),0)]), np.zeros((1,2)), np.zeros(65537)):
with pytest.raises(ValueError): receiver.push_iq_block(invalid)
assert receiver.total_samples == 0
for unused in range(100):
assert receiver.push_iq_block(np.zeros(65536, np.complex64)) == []
assert receiver.buffer.size <= receiver.buffer_limit_samples
assert receiver.decimator.values.size <= (receiver.buffer_limit_samples + 7)//8
assert receiver.finish() == []
assert receiver.buffer.size == receiver.decimator.values.size == 0
with pytest.raises(RuntimeError): receiver.finish()
with pytest.raises(RuntimeError): receiver.push_iq_block(np.zeros(1,np.complex64))
def test_truncated_frame_cannot_reach_crc_pass():
samples = signal_with_unknown_offsets()[:100000]
receiver = lab.StreamingReceiver()
output = []
for start in range(0,len(samples),65536):
output.extend(receiver.push_iq_block(samples[start:start+65536]))
assert output + receiver.finish() == []
def test_context_restores_functions_even_on_exception():
originals = (lab.lab043._matched_waveform, lab.iq._extract_soft_window,
lab.lab045.conv.viterbi_decode_soft, lab.offline._correlation_scores)
with streaming_mode(lab.phy.PhyMode(500000)):
front = lab.BlockFrontEnd(np.zeros(100000,np.complex64),65536,Counter())
with pytest.raises(RuntimeError):
with lab.optimized_context(front): raise RuntimeError("test")
assert originals == (lab.lab043._matched_waveform, lab.iq._extract_soft_window,
lab.lab045.conv.viterbi_decode_soft, lab.offline._correlation_scores)