123 lines
5.6 KiB
Python
123 lines
5.6 KiB
Python
"""Проверки причинности и математики Lab060."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from experiments import lab060_decision_directed_carrier_tracking as lab060
|
|
|
|
|
|
def synthetic_symbols(count=4096, symbol_rate=500_000, frequency_hz=4.0, phase_rad=0.3):
|
|
indexes = np.arange(count)
|
|
expected = np.resize(lab060.lab057.EXPECTED.real, count).astype(np.float64)
|
|
values = expected * np.exp(
|
|
1j * (phase_rad + 2 * np.pi * frequency_hz * (indexes + 0.5) / symbol_rate)
|
|
)
|
|
return values, expected
|
|
|
|
|
|
def test_normalized_detector_does_not_depend_on_amplitude():
|
|
angle = 0.27
|
|
assert lab060.normalized_phase_detector(np.exp(1j * angle)) == pytest.approx(angle)
|
|
assert lab060.normalized_phase_detector(50 * np.exp(1j * angle)) == pytest.approx(angle)
|
|
assert lab060.normalized_phase_detector(-3 * np.exp(1j * angle)) == pytest.approx(angle)
|
|
|
|
|
|
def test_detector_sign_and_outlier_limit():
|
|
assert lab060.normalized_phase_detector(np.exp(0.2j)) > 0
|
|
assert lab060.normalized_phase_detector(np.exp(-0.2j)) < 0
|
|
assert lab060.normalized_phase_detector(np.exp(1.5j)) == pytest.approx(lab060.DETECTOR_CLIP_RAD)
|
|
|
|
|
|
def test_loop_coefficients_and_units_are_rate_independent():
|
|
first = lab060.loop_parameters("T1", 0.001)
|
|
second = lab060.loop_parameters("T2", 0.001)
|
|
assert first.phase_coefficient > 0 and first.frequency_coefficient == 0
|
|
assert second.phase_coefficient > 0 and second.frequency_coefficient > 0
|
|
assert second == lab060.loop_parameters("T2", 500 / 500_000)
|
|
|
|
|
|
def test_phase_and_frequency_state_update_matches_equations():
|
|
values, _expected = synthetic_symbols(count=lab060.PREAMBLE_SYMBOLS + 2,
|
|
frequency_hz=0.0, phase_rad=0.2)
|
|
parameters = lab060.loop_parameters("T2", 0.001)
|
|
result = lab060.track_symbols(values, 500_000, 0.0, 1 + 0j, "T2", parameters)
|
|
first = lab060.PREAMBLE_SYMBOLS
|
|
error = lab060.normalized_phase_detector(result.corrected[first])
|
|
expected_next_phase = result.phase_rad[first] + parameters.phase_coefficient * error
|
|
expected_next_frequency = parameters.frequency_coefficient * error * 500_000 / (2 * np.pi)
|
|
assert result.phase_rad[first + 1] == pytest.approx(expected_next_phase)
|
|
assert result.frequency_hz[first + 1] == pytest.approx(expected_next_frequency)
|
|
|
|
|
|
def test_t0_reproduces_p3_fixed_compensation():
|
|
values, _expected = synthetic_symbols(frequency_hz=3.5, phase_rad=-0.4)
|
|
estimate = lab060.lab059.estimate_from_burst(values, 500_000, "P3")
|
|
result = lab060.track_symbols(
|
|
values, 500_000, estimate.frequency_hz, estimate.gain,
|
|
"T0", lab060.loop_parameters("T0", 0.0),
|
|
)
|
|
reference = lab060.lab059.compensate(values, 500_000, estimate)
|
|
assert np.max(abs(result.corrected - reference)) < 1e-12
|
|
|
|
|
|
@pytest.mark.parametrize("initial_error", [-10.0, -2.0, 0.0, 2.0, 10.0])
|
|
@pytest.mark.parametrize("tracker", ["T1", "T2"])
|
|
def test_decision_directed_tracker_converges_without_payload_truth(initial_error, tracker):
|
|
values, expected = synthetic_symbols(count=30_000, frequency_hz=2.75, phase_rad=0.4)
|
|
parameters = lab060.loop_parameters(tracker, 0.001 if tracker == "T1" else 0.0002)
|
|
result = lab060.track_symbols(
|
|
values, 500_000, 2.75 + initial_error, np.exp(0.4j), tracker, parameters,
|
|
)
|
|
decisions = result.hard_decisions[lab060.PREAMBLE_SYMBOLS:]
|
|
truth = np.where(expected[lab060.PREAMBLE_SYMBOLS:] >= 0, 1, -1)
|
|
assert np.mean(decisions != truth) < 0.01
|
|
|
|
|
|
def test_payload_truth_reference_cannot_change_tracker_trajectory():
|
|
values, expected = synthetic_symbols(count=6000, frequency_hz=4.0, phase_rad=0.2)
|
|
parameters = lab060.loop_parameters("T2", 0.0005)
|
|
first = lab060.track_symbols(values, 500_000, 1.0, np.exp(0.2j), "T2", parameters)
|
|
changed_truth = expected.copy()
|
|
changed_truth[lab060.PREAMBLE_SYMBOLS:] *= -1
|
|
first_metrics, _ = lab060.evaluate_tracking(first, expected, 500_000)
|
|
second = lab060.track_symbols(values, 500_000, 1.0, np.exp(0.2j), "T2", parameters)
|
|
second_metrics, _ = lab060.evaluate_tracking(second, changed_truth, 500_000)
|
|
assert np.array_equal(first.hard_decisions, second.hard_decisions)
|
|
assert np.array_equal(first.phase_rad, second.phase_rad)
|
|
assert np.array_equal(first.frequency_hz, second.frequency_hz)
|
|
assert first_metrics["symbol_error_rate"] != second_metrics["symbol_error_rate"]
|
|
|
|
|
|
def test_loss_of_lock_and_recovery_criterion_is_crc_independent():
|
|
block_ser = np.zeros(16)
|
|
block_ser[4:6] = 0.5
|
|
block_ser[8:10] = 0.05
|
|
metrics = lab060.loss_metrics(block_ser, 0.01)
|
|
assert metrics["loss_of_lock"]
|
|
assert metrics["self_recovered"]
|
|
assert metrics["longest_bad_state_seconds"] == pytest.approx(0.02)
|
|
|
|
|
|
def test_background_noise_has_no_stable_receiver_lock():
|
|
random_generator = np.random.default_rng(6009)
|
|
count = 20_000
|
|
noise = random_generator.normal(size=count) + 1j * random_generator.normal(size=count)
|
|
result = lab060.track_symbols(
|
|
noise, 500_000, 0.0, 1 + 0j, "T1", lab060.loop_parameters("T1", 0.0002)
|
|
)
|
|
assert not lab060.receiver_lock_metrics(result.detector_error_rad)["stable_lock"]
|
|
|
|
|
|
def test_synthetic_seed_is_deterministic():
|
|
first, expected_first = lab060.synthetic_received(20_000, 0.55, 123, 2.75, 0.4)
|
|
second, expected_second = lab060.synthetic_received(20_000, 0.55, 123, 2.75, 0.4)
|
|
assert np.array_equal(first, second)
|
|
assert np.array_equal(expected_first, expected_second)
|
|
|
|
|
|
def test_candidate_bandwidths_are_small_fixed_set():
|
|
assert lab060.NORMALIZED_BANDWIDTHS == (0.0002, 0.0005, 0.001, 0.002, 0.004)
|
|
assert len(lab060.NORMALIZED_BANDWIDTHS) == 5
|