Files
SDR-Rover/tests/test_lab060_decision_directed_carrier_tracking.py
2026-09-11 14:13:51 +03:00

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