Lab046C: recover short 1400 MHz capture offline
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import struct
|
||||
from types import SimpleNamespace
|
||||
import zlib
|
||||
|
||||
import numpy as np
|
||||
@@ -128,6 +129,39 @@ def test_calibration_uses_only_fixed_local_peak_quantile(monkeypatch) -> None:
|
||||
assert statistics.candidate_count == int(np.count_nonzero(peaks >= rule.threshold))
|
||||
|
||||
|
||||
def test_1400mhz_threshold_formula_is_reproducible() -> None:
|
||||
peaks = np.asarray([0.01, 0.02, 0.021, 0.022, 0.024], dtype=np.float32)
|
||||
first = offline.candidate_threshold_from_local_peaks(
|
||||
peaks,
|
||||
quantile=offline.BACKGROUND_1400MHZ_LOCAL_PEAK_QUANTILE,
|
||||
mad_multiplier=offline.BACKGROUND_1400MHZ_MAD_MULTIPLIER,
|
||||
)
|
||||
second = offline.candidate_threshold_from_local_peaks(
|
||||
peaks.copy(),
|
||||
quantile=offline.BACKGROUND_1400MHZ_LOCAL_PEAK_QUANTILE,
|
||||
mad_multiplier=offline.BACKGROUND_1400MHZ_MAD_MULTIPLIER,
|
||||
)
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_1400mhz_control_background_creates_no_candidates(monkeypatch) -> None:
|
||||
scores = np.asarray([0.001, 0.01, 0.02], dtype=np.float32)
|
||||
peaks = np.asarray([0.02, 0.02711654081940651], dtype=np.float32)
|
||||
monkeypatch.setattr(
|
||||
offline,
|
||||
"_background_peak_scores",
|
||||
lambda path, interval: (scores, peaks, 0.1),
|
||||
)
|
||||
rule = offline.CandidateRule(
|
||||
"fixed before independent decoding",
|
||||
0.999,
|
||||
offline.FIXED_1400MHZ_CANDIDATE_THRESHOLD,
|
||||
)
|
||||
statistics = offline.evaluate_1400mhz_control_background(Path("unused"), rule)
|
||||
assert statistics.candidate_count == 0
|
||||
assert max(peaks) < rule.threshold
|
||||
|
||||
|
||||
def test_control_background_cannot_change_fixed_rule(monkeypatch) -> None:
|
||||
peaks = np.asarray([0.01, 0.02, 0.20], dtype=np.float32)
|
||||
monkeypatch.setattr(
|
||||
@@ -214,6 +248,67 @@ def test_noise_candidates_never_become_valid_without_crc(
|
||||
assert result.counters.crc_pass == 0
|
||||
|
||||
|
||||
def test_inconsistent_accepted_flag_cannot_bypass_crc(tmp_path: Path) -> None:
|
||||
capture = _capture_file(tmp_path / "inconsistent_accept.cs8")
|
||||
candidate = offline.CoarseCandidate(10, 0.04)
|
||||
|
||||
def inconsistent(raw, sample_count, row, number):
|
||||
del raw, sample_count, number
|
||||
return _candidate_outcome(
|
||||
row,
|
||||
accepted=True,
|
||||
next_sample=20,
|
||||
crc=offline.lab046c.lab045.CRC_FAIL,
|
||||
)
|
||||
|
||||
result = offline.strict_process_candidates(
|
||||
capture,
|
||||
(candidate,),
|
||||
decoder=inconsistent,
|
||||
)
|
||||
assert result.counters.crc_pass == 0
|
||||
assert result.counters.crc_fail == 1
|
||||
assert result.counters.valid_frames == 0
|
||||
assert result.accepted_frames == ()
|
||||
|
||||
|
||||
def test_weak_peak_only_enters_first_stage_without_crc(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "weak_candidate.cs8")
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
scores = np.asarray([0.0, 0.04, 0.0], dtype=np.float32)
|
||||
yield 0, scores, np.asarray([1]), 3, 100
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
rule = offline.CandidateRule(
|
||||
"fixed",
|
||||
0.999,
|
||||
offline.FIXED_1400MHZ_CANDIDATE_THRESHOLD,
|
||||
)
|
||||
coarse = offline.coarse_search_capture(capture, rule)
|
||||
|
||||
def reject(raw, sample_count, candidate, number):
|
||||
del raw, sample_count, number
|
||||
return _candidate_outcome(
|
||||
candidate,
|
||||
accepted=False,
|
||||
next_sample=candidate.sample_offset + 1,
|
||||
)
|
||||
|
||||
strict = offline.strict_process_candidates(
|
||||
capture,
|
||||
coarse.candidates,
|
||||
decoder=reject,
|
||||
)
|
||||
assert len(coarse.candidates) == 1
|
||||
assert strict.counters.valid_frames == 0
|
||||
assert strict.counters.crc_pass == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("header", "reason"),
|
||||
[
|
||||
@@ -404,6 +499,12 @@ def test_receiver_api_has_no_tx_or_expected_count_oracle() -> None:
|
||||
}
|
||||
assert parameters.isdisjoint(forbidden)
|
||||
|
||||
short_parameters = set(
|
||||
inspect.signature(offline.run_1400mhz_independent_recovery).parameters
|
||||
)
|
||||
assert short_parameters == {"capture_path"}
|
||||
assert short_parameters.isdisjoint(forbidden)
|
||||
|
||||
|
||||
def test_post_verdict_diagnostics_refuses_incomplete_verdict(tmp_path: Path) -> None:
|
||||
result = object.__new__(offline.IndependentRecoveryResult)
|
||||
@@ -412,6 +513,38 @@ def test_post_verdict_diagnostics_refuses_incomplete_verdict(tmp_path: Path) ->
|
||||
offline.diagnose_after_independent_verdict(result, tmp_path)
|
||||
|
||||
|
||||
def test_1400mhz_missing_frames_report_error_counts_as_unavailable(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
payload = bytes(256)
|
||||
plan = offline.lab046c.lab045.build_transmission_plan(payload)
|
||||
(tmp_path / "tx_payload_0.bin").write_bytes(payload)
|
||||
(tmp_path / "tx_packet_0.bin").write_bytes(plan.packet)
|
||||
independent = object.__new__(offline.IndependentRecoveryResult)
|
||||
object.__setattr__(independent, "completed", True)
|
||||
object.__setattr__(
|
||||
independent,
|
||||
"strict",
|
||||
SimpleNamespace(accepted_frames=()),
|
||||
)
|
||||
diagnostics = offline.diagnose_1400mhz_after_independent_verdict(
|
||||
independent,
|
||||
tmp_path,
|
||||
)
|
||||
assert diagnostics.matched_valid_frames == 0
|
||||
assert diagnostics.missed_tx_frames == (0,)
|
||||
assert diagnostics.total_pre_fec_coded_bit_errors is None
|
||||
assert diagnostics.total_post_fec_protocol_bit_errors is None
|
||||
assert diagnostics.total_payload_bit_errors is None
|
||||
|
||||
|
||||
def test_1400mhz_main_persists_independent_result_before_tx_reference() -> None:
|
||||
source = inspect.getsource(offline.main)
|
||||
write_position = source.index("write_1400mhz_independent_result")
|
||||
diagnostic_position = source.index("diagnose_1400mhz_after_independent_verdict")
|
||||
assert write_position < diagnostic_position
|
||||
|
||||
|
||||
def test_safety_limited_independent_run_does_not_unlock_tx_reference(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user