156 lines
6.7 KiB
Python
156 lines
6.7 KiB
Python
"""Проверки математики и информационных границ Lab059."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from experiments import lab059_preamble_carrier_limits as lab059
|
|
|
|
|
|
@pytest.mark.parametrize("frequency_hz", [-100.0, -5.0, -0.25, 0.0, 0.5, 20.0])
|
|
def test_phase_slope_conversion_sign_units_and_square_factor(frequency_hz):
|
|
ordinary = 2 * np.pi * frequency_hz
|
|
squared = 4 * np.pi * frequency_hz
|
|
assert lab059.phase_slope_to_hz(ordinary, squared_bpsk=False) == pytest.approx(frequency_hz)
|
|
assert lab059.phase_slope_to_hz(squared, squared_bpsk=True) == pytest.approx(frequency_hz)
|
|
if frequency_hz:
|
|
assert lab059.phase_slope_to_hz(squared, squared_bpsk=False) == pytest.approx(2 * frequency_hz)
|
|
|
|
|
|
def test_phase_drift_and_frequency_limit_are_inverse():
|
|
for phase_limit in (15, 30, 45, 60, 90):
|
|
frequency = lab059.maximum_frequency_error_hz(
|
|
phase_limit, lab059.BURST_DURATION_SECONDS
|
|
)
|
|
assert lab059.phase_drift_degrees(
|
|
frequency, lab059.BURST_DURATION_SECONDS
|
|
) == pytest.approx(phase_limit)
|
|
assert lab059.maximum_frequency_error_hz(30, 0.2048) == pytest.approx(0.4069010416666667)
|
|
|
|
|
|
def test_awgn_crlb_scale_matches_rate_and_observed_b_quality():
|
|
slow = lab059.awgn_cfo_crlb_standard_deviation_hz(0.845350, 1280, 20_000)
|
|
fast = lab059.awgn_cfo_crlb_standard_deviation_hz(0.845350, 1280, 500_000)
|
|
assert fast / slow == pytest.approx(25.0)
|
|
assert fast == pytest.approx(3.5982182026)
|
|
|
|
|
|
@pytest.mark.parametrize("symbol_rate", [20_000, 500_000])
|
|
@pytest.mark.parametrize("method", lab059.METHODS)
|
|
def test_clean_known_cfo_reference_time_and_compensation(symbol_rate, method):
|
|
frequency_hz = 7.25 if symbol_rate == 20_000 else -37.5
|
|
phase_at_zero = -0.73
|
|
indexes = np.arange(lab059.PREAMBLE_SYMBOLS)
|
|
times = (indexes + 0.5) / symbol_rate
|
|
symbols = lab059.lab057.EXPECTED * np.exp(
|
|
1j * (phase_at_zero + 2 * np.pi * frequency_hz * times)
|
|
)
|
|
estimate = lab059.estimate_carrier(symbols, symbol_rate, method)
|
|
assert estimate.frequency_hz == pytest.approx(frequency_hz, abs=1e-8)
|
|
assert np.angle(estimate.gain * np.exp(-1j * phase_at_zero)) == pytest.approx(0.0, abs=1e-10)
|
|
corrected = lab059.compensate(symbols, symbol_rate, estimate)
|
|
assert np.max(abs(corrected - lab059.lab057.EXPECTED)) < 1e-9
|
|
|
|
|
|
def test_p0_matches_active_two_block_formula_and_ambiguity():
|
|
symbol_rate = 500_000
|
|
frequency_hz = 41.25
|
|
times = (np.arange(lab059.PREAMBLE_SYMBOLS) + 0.5) / symbol_rate
|
|
symbols = lab059.lab057.EXPECTED * np.exp(2j * np.pi * frequency_hz * times)
|
|
expected = lab059.lab057.squared_cfo_statistics(symbols, symbol_rate)
|
|
estimate = lab059.estimate_carrier(symbols, symbol_rate, "P0")
|
|
assert estimate.frequency_hz == pytest.approx(expected["cfo_hz"], abs=1e-12)
|
|
assert estimate.estimation_symbols == 1024
|
|
assert estimate.unambiguous_abs_hz == pytest.approx(symbol_rate / 2048)
|
|
|
|
|
|
def test_p2_reference_time_is_global_burst_zero():
|
|
symbol_rate = 500_000
|
|
frequency_hz = -12.75
|
|
phase_at_zero = 1.12
|
|
times = (np.arange(lab059.PREAMBLE_SYMBOLS) + 0.5) / symbol_rate
|
|
symbols = lab059.lab057.EXPECTED * np.exp(
|
|
1j * (phase_at_zero + 2 * np.pi * frequency_hz * times)
|
|
)
|
|
estimate = lab059.estimate_carrier(symbols, symbol_rate, "P2")
|
|
assert estimate.frequency_hz == pytest.approx(frequency_hz, abs=1e-9)
|
|
assert np.angle(estimate.gain * np.exp(-1j * phase_at_zero)) == pytest.approx(0.0, abs=1e-10)
|
|
assert estimate.validation_errors == 0
|
|
|
|
|
|
@pytest.mark.parametrize("method", lab059.METHODS)
|
|
def test_payload_truth_cannot_change_carrier_estimate(method):
|
|
symbol_rate = 500_000
|
|
indexes = np.arange(4096)
|
|
expected = np.resize(lab059.lab057.EXPECTED, len(indexes))
|
|
values = expected * np.exp(1j * (0.4 + 2 * np.pi * 3.5 * (indexes + 0.5) / symbol_rate))
|
|
changed = values.copy()
|
|
random_generator = np.random.default_rng(5907)
|
|
changed[lab059.PREAMBLE_SYMBOLS:] = (
|
|
random_generator.normal(size=len(changed) - lab059.PREAMBLE_SYMBOLS)
|
|
+ 1j * random_generator.normal(size=len(changed) - lab059.PREAMBLE_SYMBOLS)
|
|
) * 100
|
|
first = lab059.estimate_from_burst(values, symbol_rate, method)
|
|
second = lab059.estimate_from_burst(changed, symbol_rate, method)
|
|
assert first == second
|
|
|
|
|
|
def test_raw_payload_cannot_change_new_preamble_only_estimates():
|
|
symbol_rate = 500_000
|
|
values, _truth = lab059.lab057.synthetic_waveform(
|
|
symbol_rate, delay_symbols=0.0, cfo_hz=3.5, phase_rad=0.4, es_n0_db=None
|
|
)
|
|
frontend = lab059.lab057.Frontend(symbol_rate)
|
|
probe = values[: (4096 + 42) * frontend.samples_per_symbol]
|
|
stock = frontend.extract(probe, 4096)
|
|
matched = frontend.filter(probe)
|
|
symbols = lab059._extract_symbols(
|
|
matched, frontend.nominal, stock.timing_offset_samples,
|
|
4096, frontend.samples_per_symbol,
|
|
)
|
|
changed = probe.copy()
|
|
payload_start = (
|
|
frontend.nominal + stock.timing_offset_samples
|
|
+ lab059.PREAMBLE_SYMBOLS * frontend.samples_per_symbol
|
|
)
|
|
changed[payload_start + len(frontend.taps):] += 10 + 7j
|
|
first = lab059.real_carrier_estimates(
|
|
probe, frontend, stock.timing_offset_samples, symbols, symbol_rate, ("P1", "P2", "P3")
|
|
)
|
|
second = lab059.real_carrier_estimates(
|
|
changed, frontend, stock.timing_offset_samples, symbols, symbol_rate, ("P1", "P2", "P3")
|
|
)
|
|
assert first == second
|
|
|
|
|
|
def test_phase_extrapolation_uses_one_compensation_only():
|
|
symbol_rate = 500_000
|
|
count = 102_400
|
|
frequency_hz = 4.7
|
|
phase = 0.8
|
|
indexes = np.arange(count)
|
|
expected = np.resize(lab059.lab057.EXPECTED, count)
|
|
symbols = expected * np.exp(1j * (phase + 2 * np.pi * frequency_hz * (indexes + 0.5) / symbol_rate))
|
|
estimate = lab059.estimate_from_burst(symbols, symbol_rate, "P1")
|
|
corrected = lab059.compensate(symbols, symbol_rate, estimate)
|
|
assert np.max(abs(corrected - expected)) < 1e-8
|
|
|
|
|
|
def test_fixed_split_is_disjoint_and_not_result_dependent():
|
|
assert len(lab059.P2_TRAIN) == len(lab059.P2_VALIDATION) == 576
|
|
assert lab059.P2_TRAIN[0] == 32 and lab059.P2_TRAIN[-1] == 607
|
|
assert lab059.P2_VALIDATION[0] == 672 and lab059.P2_VALIDATION[-1] == 1247
|
|
assert set(lab059.P2_TRAIN).isdisjoint(lab059.P2_VALIDATION)
|
|
|
|
|
|
def test_partial_baseline_summary_does_not_create_nan_for_unrun_methods():
|
|
row = {
|
|
"dataset": "Lab056", "label": "B1", "method": "P0",
|
|
"frequency_error_hz": 1.0, "symbol_error_rate": 0.5,
|
|
"evm_rms": 1.0, "matched_residual_db": 0.0,
|
|
}
|
|
summary = lab059.summarize_real([row])
|
|
assert set(summary["Lab056"]) == {"P0"}
|
|
assert summary["Lab054"] == {} and summary["Lab047"] == {}
|