105 lines
4.3 KiB
Python
105 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from galileo_rx.tracking.lock_detector import BocLockDetectorConfig, LockDetector
|
|
from galileo_rx.types import CorrelatorOutputs
|
|
|
|
|
|
def _correlators(phase_rad: float) -> CorrelatorOutputs:
|
|
prompt = complex(np.exp(1j * phase_rad))
|
|
return CorrelatorOutputs(0.2 * prompt, 0.5 * prompt, prompt, 0.5 * prompt, 0.2 * prompt)
|
|
|
|
|
|
def test_data_lock_detector_ignores_bpsk_transition_but_detects_quadrature_jump() -> None:
|
|
detector = LockDetector(data_sign_insensitive=True)
|
|
detector.update(_correlators(0.0))
|
|
assert detector.update(_correlators(np.pi)).cycle_slip is False
|
|
assert detector.update(_correlators(np.pi + 0.49 * np.pi)).cycle_slip is True
|
|
|
|
|
|
def _detector() -> LockDetector:
|
|
return LockDetector(
|
|
data_sign_insensitive=True,
|
|
config=BocLockDetectorConfig(
|
|
smoothing_epochs=4,
|
|
confirmation_epochs=2,
|
|
failure_epochs=3,
|
|
),
|
|
)
|
|
|
|
|
|
def _repeat(detector: LockDetector, values: CorrelatorOutputs, count: int):
|
|
return [detector.update(values) for _ in range(count)]
|
|
|
|
|
|
def test_boc_lock_metric_validity_is_separate_from_lock_state() -> None:
|
|
detector = _detector()
|
|
statuses = _repeat(detector, _correlators(0.0), 5)
|
|
assert [status.metric_valid for status in statuses] == [False, False, False, True, True]
|
|
assert statuses[3].code_locked is False
|
|
assert statuses[4].code_locked is True
|
|
|
|
|
|
def test_boc_quarter_chip_shoulder_does_not_cause_false_unlock() -> None:
|
|
detector = _detector()
|
|
centered = CorrelatorOutputs(0.25 + 0j, 0.25 + 0j, 1.0 + 0j, 0.25 + 0j, 0.25 + 0j)
|
|
_repeat(detector, centered, 8)
|
|
# Ideal BOC(1,1) at +0.25 chip: one E/L tap is at the main peak,
|
|
# while P is only 0.25 in amplitude. The legacy P/E/L ratio is 0.1.
|
|
shoulder = CorrelatorOutputs(-0.5 + 0j, 1.0 + 0j, 0.25 + 0j, -0.5 + 0j, 0j)
|
|
statuses = _repeat(detector, shoulder, 12)
|
|
assert min(status.code_metric for status in statuses) < 0.11
|
|
assert all(status.code_locked for status in statuses)
|
|
assert not any(status.side_peak_suspected for status in statuses)
|
|
|
|
|
|
def test_boc_outer_tap_side_peak_and_signal_loss_are_detected_after_debounce() -> None:
|
|
detector = _detector()
|
|
centered = CorrelatorOutputs(0.25 + 0j, 0.25 + 0j, 1.0 + 0j, 0.25 + 0j, 0.25 + 0j)
|
|
_repeat(detector, centered, 8)
|
|
side_peak = CorrelatorOutputs(1.0 + 0j, 0.5 + 0j, 0.25 + 0j, 0j, 0j)
|
|
side_status = _repeat(detector, side_peak, 8)
|
|
assert any(status.side_peak_suspected for status in side_status)
|
|
assert side_status[-1].code_locked is False
|
|
|
|
detector = _detector()
|
|
_repeat(detector, centered, 8)
|
|
lost = _repeat(detector, CorrelatorOutputs(), 8)
|
|
assert lost[-1].signal_present is False
|
|
assert lost[-1].code_locked is False
|
|
|
|
|
|
def test_boc_negative_half_chip_prompt_side_peak_is_detected_by_tap_coherence() -> None:
|
|
detector = _detector()
|
|
centered = CorrelatorOutputs(-0.25 + 0j, 0.25 + 0j, 1.0 + 0j, 0.25 + 0j, -0.25 + 0j)
|
|
_repeat(detector, centered, 8)
|
|
# At +0.5 chip the Prompt is the negative BOC sidelobe. E and L have
|
|
# opposing signs, so powers alone are ambiguous but Re{(E+L)P*} is zero.
|
|
half_chip_side = CorrelatorOutputs(0.25 + 0j, 0.25 + 0j, -0.5 + 0j, -0.25 + 0j, 0j)
|
|
statuses = _repeat(detector, half_chip_side, 8)
|
|
assert statuses[-1].boc_main_peak_coherence <= 0.15
|
|
assert statuses[-1].side_peak_suspected is True
|
|
assert statuses[-1].code_locked is False
|
|
|
|
|
|
def test_single_bad_boc_epoch_is_debounced() -> None:
|
|
detector = _detector()
|
|
centered = CorrelatorOutputs(0.25 + 0j, 0.25 + 0j, 1.0 + 0j, 0.25 + 0j, 0.25 + 0j)
|
|
_repeat(detector, centered, 8)
|
|
side_peak = CorrelatorOutputs(1.0 + 0j, 0.5 + 0j, 0.25 + 0j, 0j, 0j)
|
|
assert detector.update(side_peak).code_locked is True
|
|
assert detector.update(centered).code_locked is True
|
|
|
|
|
|
def test_invalid_integration_does_not_enter_power_smoother() -> None:
|
|
detector = _detector()
|
|
centered = CorrelatorOutputs(0.25 + 0j, 0.25 + 0j, 1.0 + 0j, 0.25 + 0j, 0.25 + 0j)
|
|
for _ in range(10):
|
|
status = detector.update(centered, metric_input_valid=False)
|
|
assert status.metric_valid is False
|
|
assert status.code_locked is False
|
|
statuses = _repeat(detector, centered, 5)
|
|
assert statuses[-1].metric_valid is True
|
|
assert statuses[-1].code_locked is True
|