Lab047: transmit JPEG over real RF link
This commit is contained in:
@@ -428,6 +428,211 @@ def test_cursor_is_strictly_monotonic(tmp_path: Path) -> None:
|
||||
assert all(after > before for before, after in zip(result.cursor_history, result.cursor_history[1:]))
|
||||
|
||||
|
||||
def test_streaming_more_than_ten_thousand_candidates_reach_strict_stage(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "many_candidates.cs8", 250_000)
|
||||
candidate_count = 10_001
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
scores = np.ones(candidate_count, dtype=np.float32)
|
||||
yield 0, scores, np.arange(candidate_count), 250_000, 1_000
|
||||
|
||||
processed = 0
|
||||
|
||||
def reject(raw, sample_count, candidate, number):
|
||||
nonlocal processed
|
||||
del raw, sample_count, number
|
||||
processed += 1
|
||||
return _candidate_outcome(
|
||||
candidate,
|
||||
accepted=False,
|
||||
next_sample=candidate.sample_offset + 1,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
coarse, strict = offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
max_candidates=20_000,
|
||||
decoder=reject,
|
||||
)
|
||||
|
||||
assert coarse.candidate_count == candidate_count
|
||||
assert coarse.max_candidates_reached is False
|
||||
assert strict.counters.candidates_processed == candidate_count
|
||||
assert processed == candidate_count
|
||||
assert strict.outcomes_truncated is True
|
||||
|
||||
|
||||
def test_streaming_candidates_are_checked_before_next_block(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "stream_order.cs8", 500)
|
||||
events: list[str] = []
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
yield 0, np.ones(2, np.float32), np.array([0, 1]), 100, 100
|
||||
assert events == ["decode:0", "decode:20"]
|
||||
events.append("second_block")
|
||||
yield 100, np.ones(1, np.float32), np.array([0]), 100, 100
|
||||
|
||||
def reject(raw, sample_count, candidate, number):
|
||||
del raw, sample_count, number
|
||||
events.append(f"decode:{candidate.sample_offset}")
|
||||
return _candidate_outcome(candidate, accepted=False, next_sample=0)
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
decoder=reject,
|
||||
)
|
||||
|
||||
assert events == ["decode:0", "decode:20", "second_block", "decode:100"]
|
||||
|
||||
|
||||
def test_streaming_accepted_frame_owns_following_local_maxima(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "owned_event.cs8", 1_000)
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
scores = np.ones(31, np.float32)
|
||||
yield 0, scores, np.array([5, 6, 10, 24, 30]), 1_000, 100
|
||||
|
||||
def decoder(raw, sample_count, candidate, number):
|
||||
del raw, sample_count, number
|
||||
return _candidate_outcome(
|
||||
candidate,
|
||||
accepted=candidate.sample_offset == 100,
|
||||
next_sample=500 if candidate.sample_offset == 100 else 0,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
coarse, strict = offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
decoder=decoder,
|
||||
)
|
||||
|
||||
assert strict.counters.valid_frames == 1
|
||||
assert strict.counters.candidates_processed == 2
|
||||
assert coarse.candidates_skipped_as_same_local_event == 3
|
||||
|
||||
|
||||
def test_streaming_exact_overlap_duplicate_is_checked_once(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "exact_overlap.cs8", 500)
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
yield 0, np.ones(6, np.float32), np.array([5]), 100, 100
|
||||
yield 100, np.ones(1, np.float32), np.array([0]), 100, 100
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
coarse, strict = offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
decoder=lambda raw, count, candidate, number: _candidate_outcome(
|
||||
candidate,
|
||||
accepted=False,
|
||||
next_sample=0,
|
||||
),
|
||||
)
|
||||
|
||||
assert coarse.candidate_count == 1
|
||||
assert coarse.exact_duplicate_candidates_skipped == 1
|
||||
assert strict.counters.candidates_processed == 1
|
||||
|
||||
|
||||
def test_streaming_bad_candidate_cannot_hide_later_valid_frame(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "bad_then_valid.cs8", 500)
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
yield 0, np.ones(3, np.float32), np.array([0, 1, 2]), 100, 100
|
||||
|
||||
def decoder(raw, sample_count, candidate, number):
|
||||
del raw, sample_count, number
|
||||
return _candidate_outcome(
|
||||
candidate,
|
||||
accepted=candidate.sample_offset == 20,
|
||||
next_sample=10_000,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
_, strict = offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
decoder=decoder,
|
||||
)
|
||||
|
||||
assert strict.counters.candidates_processed == 2
|
||||
assert strict.counters.valid_frames == 1
|
||||
assert strict.accepted_frames[0].refined_start_sample == 20
|
||||
|
||||
|
||||
def test_streaming_max_candidates_limits_strict_checks(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "stream_limit.cs8", 500)
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
yield 0, np.ones(4, np.float32), np.arange(4), 100, 100
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
coarse, strict = offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
max_candidates=2,
|
||||
decoder=lambda raw, count, candidate, number: _candidate_outcome(
|
||||
candidate,
|
||||
accepted=False,
|
||||
next_sample=0,
|
||||
),
|
||||
)
|
||||
|
||||
assert coarse.max_candidates_reached is True
|
||||
assert coarse.termination_reason == "MAX_CANDIDATES_REACHED"
|
||||
assert strict.counters.candidates_processed == 2
|
||||
assert strict.max_search_iterations_reached is True
|
||||
|
||||
|
||||
def test_streaming_exhausted_blocks_cover_full_capture_not_last_correlation_start(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
capture = _capture_file(tmp_path / "full_coverage.cs8", 500)
|
||||
|
||||
def blocks(raw, interval, chunk_samples):
|
||||
del raw, interval, chunk_samples
|
||||
yield 0, np.zeros(1, np.float32), np.array([], dtype=np.int64), 500, 100
|
||||
|
||||
monkeypatch.setattr(offline, "_score_blocks", blocks)
|
||||
coarse, strict = offline.stream_process_capture(
|
||||
capture,
|
||||
offline.CandidateRule("fixed", 0.999, 0.5),
|
||||
)
|
||||
|
||||
assert coarse.processed_until_sample == 500
|
||||
assert coarse.termination_reason == "END_OF_CAPTURE"
|
||||
assert strict.termination_reason == "END_OF_CAPTURE"
|
||||
|
||||
|
||||
def test_max_candidates_fails_closed(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -572,9 +777,26 @@ def test_safety_limited_independent_run_does_not_unlock_tx_reference(
|
||||
"MAX_CANDIDATES_REACHED",
|
||||
True,
|
||||
)
|
||||
strict = offline.StrictSearchResult(
|
||||
counters=offline.StrictCounters(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
accepted_frames=(),
|
||||
outcomes=(),
|
||||
cursor_history=(0,),
|
||||
elapsed_seconds=0.0,
|
||||
average_candidate_seconds=0.0,
|
||||
maximum_local_window_samples=0,
|
||||
maximum_local_window_bytes=0,
|
||||
termination_reason="MAX_CANDIDATES_REACHED",
|
||||
max_search_iterations_reached=True,
|
||||
max_valid_frames_reached=False,
|
||||
)
|
||||
monkeypatch.setattr(offline, "calibrate_candidate_rule", lambda path: (rule, background))
|
||||
monkeypatch.setattr(offline, "evaluate_control_background", lambda path, fixed: background)
|
||||
monkeypatch.setattr(offline, "coarse_search_capture", lambda path, fixed: coarse)
|
||||
monkeypatch.setattr(
|
||||
offline,
|
||||
"stream_process_capture",
|
||||
lambda path, fixed: (coarse, strict),
|
||||
)
|
||||
result = offline.run_independent_recovery(capture)
|
||||
assert result.completed is False
|
||||
with pytest.raises(RuntimeError, match="verdict"):
|
||||
|
||||
Reference in New Issue
Block a user