129 lines
6.2 KiB
Python
129 lines
6.2 KiB
Python
"""Проверки диагностических оценок без сохранённых IQ и оборудования."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from experiments import lab057_symbol_diagnosis as diagnosis
|
|
|
|
|
|
@pytest.mark.parametrize("symbol_rate", [20_000, 500_000])
|
|
@pytest.mark.parametrize("sign", [-1, 1])
|
|
def test_frequency_phase_units_and_compensation(symbol_rate, sign):
|
|
frequency = sign * 37.5 * symbol_rate / 500_000
|
|
phase = -.8
|
|
times = (np.arange(1280) + .5) / symbol_rate
|
|
symbols = diagnosis.EXPECTED * np.exp(1j * (phase + 2*np.pi*frequency*times))
|
|
found, gain, score, boundary = diagnosis.frequency_search(symbols[diagnosis.TRAIN], symbol_rate)
|
|
assert abs(found - frequency) < .01
|
|
assert abs(np.angle(gain) - phase) < .001
|
|
assert score > .99999 and not boundary
|
|
estimate = diagnosis.Estimate(0, found, gain, score)
|
|
corrected = diagnosis.compensate(symbols, symbol_rate, estimate)
|
|
assert np.count_nonzero((corrected.real[diagnosis.VALIDATION] >= 0) != diagnosis.PREAMBLE[diagnosis.VALIDATION]) == 0
|
|
|
|
|
|
@pytest.mark.parametrize("symbol_rate", [20_000, 500_000])
|
|
def test_squared_cfo_alias_and_factor_two(symbol_rate):
|
|
bound = symbol_rate / 2048
|
|
times = (np.arange(1280) + .5) / symbol_rate
|
|
for frequency, expected in [(bound*.4, bound*.4), (bound*1.2, -bound*.8)]:
|
|
symbols = diagnosis.EXPECTED * np.exp(2j*np.pi*frequency*times)
|
|
result = diagnosis.squared_cfo_statistics(symbols, symbol_rate)
|
|
assert result["cfo_hz"] == pytest.approx(expected, abs=1e-9)
|
|
assert result["delta_time_seconds"] == 512 / symbol_rate
|
|
|
|
|
|
def test_frequency_boundary_is_reported_without_expanding_search():
|
|
times = (diagnosis.TRAIN + .5) / 500_000
|
|
symbols = diagnosis.EXPECTED[diagnosis.TRAIN] * np.exp(2j*np.pi*400*times)
|
|
frequency, _, _, boundary = diagnosis.frequency_search(symbols, 500_000)
|
|
assert boundary and frequency == 250
|
|
|
|
|
|
def test_fractional_interpolator_preserves_integer_samples_and_complex_phase():
|
|
indexes = np.arange(200)
|
|
values = np.exp(2j*np.pi*.03*indexes)
|
|
assert np.allclose(diagnosis.interpolate(values, indexes[20:180]), values[20:180], atol=1e-14)
|
|
positions = np.arange(20, 180) + .35
|
|
assert np.max(abs(diagnosis.interpolate(values, positions) - np.exp(2j*np.pi*.03*positions))) < .001
|
|
|
|
|
|
@pytest.mark.parametrize("symbol_rate", [20_000, 500_000])
|
|
def test_fractional_timing_recovery_from_waveform(symbol_rate):
|
|
values, truth = diagnosis.synthetic_waveform(symbol_rate)
|
|
frontend = diagnosis.Frontend(symbol_rate)
|
|
probe = values[:(1424+42)*frontend.samples_per_symbol]
|
|
estimate = diagnosis.estimate_variant(frontend.estimation_filter(probe), frontend.nominal,
|
|
symbol_rate, "D3", 700, 999)
|
|
assert abs(estimate.timing_samples / frontend.samples_per_symbol - truth["delay_symbols"]) <= 1/32
|
|
assert abs(estimate.cfo_hz) < symbol_rate/500_000 * 2
|
|
|
|
|
|
@pytest.mark.parametrize("variant", ["D0", "D1", "D2", "D3"])
|
|
def test_raw_validation_header_and_body_cannot_change_new_estimates(variant):
|
|
frontend = diagnosis.Frontend(500_000)
|
|
random_generator = np.random.default_rng(5717)
|
|
original = (random_generator.normal(size=23456) + 1j*random_generator.normal(size=23456)).astype(np.complex64)
|
|
altered = original.copy()
|
|
low, high = frontend.train_support()
|
|
altered[:low] += 10+7j
|
|
altered[high:] += 10+7j
|
|
first = diagnosis.estimate_variant(frontend.estimation_filter(original), frontend.nominal, 500_000, variant, 0, 17)
|
|
second = diagnosis.estimate_variant(frontend.estimation_filter(altered), frontend.nominal, 500_000, variant, 0, 17)
|
|
assert first == second
|
|
|
|
|
|
def test_d3_does_not_inherit_stock_timing_or_cfo():
|
|
values, _ = diagnosis.synthetic_waveform(500_000)
|
|
frontend = diagnosis.Frontend(500_000)
|
|
matched = frontend.estimation_filter(values[:23456])
|
|
assert diagnosis.estimate_variant(matched, frontend.nominal, 500_000, "D3", 0, 0) == diagnosis.estimate_variant(
|
|
matched, frontend.nominal, 500_000, "D3", 800, -200)
|
|
|
|
|
|
def test_reference_file_changes_only_post_decision_error_counts(tmp_path):
|
|
values, _ = diagnosis.synthetic_waveform(500_000, delay_symbols=0)
|
|
reader = diagnosis.ArrayFile(values)
|
|
path = tmp_path / "expected_header_and_jpeg.json"
|
|
expected = diagnosis.historical.plans()[0].plan.phy_header_bits
|
|
|
|
def analyze(bits, jpeg):
|
|
path.write_text(json.dumps({"header": bits.tolist(), "jpeg": jpeg}), encoding="utf-8")
|
|
truth = json.loads(path.read_text(encoding="utf-8"))
|
|
return diagnosis.analyze_frame(reader, 0, 500_000, "TEST", 1,
|
|
np.array(truth["header"]), write_packets=False)[0]
|
|
|
|
first = analyze(expected, "original")
|
|
second = analyze(1-expected, "different bytes")
|
|
keys = ("timing_samples", "cfo_hz", "phase_rad", "gain_abs", "HEADER_ACCEPTED", "CRC_RESULT")
|
|
assert all(all(a[key] == b[key] for key in keys) for a, b in zip(first, second))
|
|
assert first[0]["header_voted_errors"] != second[0]["header_voted_errors"]
|
|
|
|
|
|
def test_failed_header_never_starts_fec_or_crc():
|
|
result, output = diagnosis.decode_body(np.zeros(144))
|
|
assert not result["HEADER_ACCEPTED"]
|
|
assert not result["FEC_STARTED"] and not result["CRC_STARTED"]
|
|
assert result["CRC_RESULT"] == "NOT_REACHED" and output is None
|
|
|
|
|
|
def test_truncated_stock_noise_probe_is_not_repaired():
|
|
random_generator = np.random.default_rng(5730)
|
|
values = (random_generator.normal(size=92256) + 1j*random_generator.normal(size=92256)).astype(np.complex64)
|
|
rows, _, _ = diagnosis.analyze_frame(diagnosis.ArrayFile(values), 0, 500_000, "NOISE", 1, None, write_packets=False)
|
|
assert not any(row["PACKET_OUTPUT"] for row in rows)
|
|
assert all(not row["HEADER_ACCEPTED"] for row in rows)
|
|
|
|
|
|
def test_equal_es_n0_is_not_equal_noise_variance_across_rates():
|
|
_, slow = diagnosis.synthetic_waveform(20_000, es_n0_db=12)
|
|
_, fast = diagnosis.synthetic_waveform(500_000, es_n0_db=12)
|
|
for truth in [slow, fast]:
|
|
ratio = truth["signal_power"] * (diagnosis.FS / truth["symbol_rate"]) / truth["complex_noise_variance"]
|
|
assert 10*np.log10(ratio) == pytest.approx(12)
|
|
assert slow["complex_noise_variance"] / fast["complex_noise_variance"] == pytest.approx(25, rel=.01)
|