Lab046A: validate continuous soft-symbol frame scanning

This commit is contained in:
LittleSam129
2026-09-01 13:18:42 +03:00
parent 1027eef53d
commit e8d85ae4b7
4 changed files with 879 additions and 6 deletions

View File

@@ -7,6 +7,7 @@
from __future__ import annotations
import inspect
import struct
import numpy as np
@@ -243,3 +244,224 @@ def test_receiver_ignores_original_object_on_corruption() -> None:
def test_functional_self_check_runs() -> None:
lab046.run_functional_tests()
# ------------------------------------------------------------ Lab046A: continuous stream
def _small_continuous_stream(
*,
leading: int = 0,
gaps: dict[int, int] | None = None,
trailing: int = 0,
) -> tuple[bytes, lab046.ContinuousSoftStreamPlan]:
obj = lab046.make_deterministic_object(300, seed=460)
plan = lab046.build_continuous_soft_stream(
lab046.build_object_frames(obj),
leading_background_symbols=leading,
inter_frame_background=gaps,
trailing_background_symbols=trailing,
seed=461,
)
return obj, plan
def test_two_sequential_frames_found_without_known_offsets() -> None:
obj, stream_plan = _small_continuous_stream()
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert received.valid_frame_count == 2
assert received.recovered_object == obj
# Сравнение с TX offsets разрешено только здесь, после независимого receive.
assert received.candidate_starts == stream_plan.frame_starts
def test_many_sequential_frames_are_found() -> None:
obj = lab046.make_deterministic_object(244 * 10, seed=462)
result = lab046.run_continuous_object_transport(obj)
assert result.receive.valid_frame_count == 10
assert result.receive.preamble_candidates == 10
assert result.sha_match
def test_first_frame_need_not_start_at_zero() -> None:
obj, stream_plan = _small_continuous_stream(leading=37)
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert received.candidate_starts[0] == 37
assert received.recovered_object == obj
def test_trailing_noise_is_ignored() -> None:
obj, stream_plan = _small_continuous_stream(trailing=43)
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert received.valid_frame_count == 2
assert received.preamble_candidates == 2
assert received.recovered_object == obj
def test_inter_frame_background_does_not_hide_next_frame() -> None:
obj, stream_plan = _small_continuous_stream(gaps={0: 19})
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert received.candidate_starts[1] - received.candidate_starts[0] == 5724 + 19
assert received.recovered_object == obj
def test_same_frame_is_not_decoded_twice() -> None:
_, stream_plan = _small_continuous_stream()
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert len(received.candidate_starts) == len(set(received.candidate_starts))
assert received.preamble_candidates == received.valid_frame_count == 2
def test_malformed_candidate_rejected_and_later_good_frame_found() -> None:
obj = lab046.make_deterministic_object(300, seed=463)
result = lab046.run_continuous_object_transport(
obj,
prepend_malformed_candidate=True,
)
assert result.receive.rejected_candidates > 0
assert result.receive.valid_frame_count == 2
assert result.receive.candidate_starts[0] == 0
assert result.sha_match
def test_stronger_false_peak_does_not_delete_overlapping_later_frame() -> None:
obj = lab046.make_deterministic_object(100, seed=999)
frame = lab046.build_object_frames(obj)[0]
preamble = lab046._bits_to_float32_soft(lab046.lab045.lab044.preamble_bits())
true_frame = lab046._bits_to_float32_soft(frame.plan.frame_bits)
true_start = 508 # четыре периода PRBS7 после ложного старта
stream = np.zeros(true_start + true_frame.size, dtype=np.float32)
stream[:preamble.size] = preamble
stream[true_start:true_start + true_frame.size] = true_frame
stream[preamble.size:true_start + preamble.size] *= np.float32(0.7)
scores = lab046._normalized_soft_preamble_correlation(stream)
assert scores[0] > scores[true_start]
received = lab046.scan_continuous_soft_stream(stream)
assert received.rejected_candidates > 0
assert true_start in received.candidate_starts
assert received.valid_frame_count == 1
assert received.recovered_object == obj
def test_validated_header_drives_candidate_body_length() -> None:
obj, stream_plan = _small_continuous_stream()
packet_bytes = lab046.lab045.MIN_PACKET_BYTES
coded_symbols = lab046.lab045.conv.encoded_bit_count(packet_bytes * 8)
prefix_bits = np.concatenate((
lab046.lab045.lab044.preamble_bits(),
lab046.lab045.encode_phy_header(packet_bytes),
np.zeros(coded_symbols, dtype=np.uint8),
))
stream = np.concatenate((
lab046._bits_to_float32_soft(prefix_bits),
stream_plan.soft_symbols,
))
received = lab046.scan_continuous_soft_stream(stream)
assert received.frame_receptions[0].packet_bytes == packet_bytes
assert received.frame_receptions[0].frame_end == len(prefix_bits)
assert received.rejected_candidates >= 1
assert received.recovered_object == obj
def test_cursor_is_strictly_monotonic() -> None:
_, stream_plan = _small_continuous_stream(leading=7, gaps={0: 13}, trailing=5)
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert all(
later > earlier
for earlier, later in zip(received.cursor_history, received.cursor_history[1:])
)
def test_max_search_iterations_is_fail_closed() -> None:
_, stream_plan = _small_continuous_stream()
received = lab046.scan_continuous_soft_stream(
stream_plan.soft_symbols,
max_search_iterations=1,
)
assert received.max_search_iterations_reached
assert received.termination_reason == "MAX_SEARCH_ITERATIONS_REACHED"
assert received.valid_frame_count == 1
assert not received.object_complete
def test_max_frames_is_fail_closed() -> None:
_, stream_plan = _small_continuous_stream()
received = lab046.scan_continuous_soft_stream(
stream_plan.soft_symbols,
max_frames=1,
)
assert received.max_frames_reached
assert received.termination_reason == "MAX_FRAMES_REACHED"
assert received.valid_frame_count == 1
assert not received.object_complete
def test_continuous_ideal_16_kib_object_is_exact() -> None:
obj = lab046.make_deterministic_object()
result = lab046.run_continuous_object_transport(obj)
assert result.receive.preamble_candidates == 68
assert result.receive.valid_phy_headers == 68
assert result.receive.fec_pass_count == 68
assert result.receive.crc_pass_count == 68
assert result.receive.valid_fragments == 68
assert result.receive.object_complete
assert result.sha_match
assert result.object_byte_errors == 0
assert result.stream_plan.soft_symbols.dtype == np.float32
assert result.stream_plan.soft_symbols.nbytes < 2_000_000
def test_continuous_background_16_kib_object_is_exact() -> None:
obj = lab046.make_deterministic_object()
result = lab046.run_continuous_object_transport(
obj,
leading_background_symbols=37,
inter_frame_background={7: 11, 23: 19, 51: 7},
trailing_background_symbols=43,
seed=464,
)
assert result.stream_plan.leading_background_symbols == 37
assert result.stream_plan.inter_frame_background_symbols == 37
assert result.stream_plan.trailing_background_symbols == 43
assert result.receive.valid_frame_count == 68
assert result.receive.object_complete
assert result.sha_match
def test_continuous_noisy_16_kib_object_is_exact_after_fec() -> None:
obj = lab046.make_deterministic_object()
result = lab046.run_continuous_object_transport(
obj,
noise_sigma=0.40,
seed=46001,
)
assert result.receive.valid_frame_count == 68
assert result.pre_fec_coded_bit_errors > 0
assert result.post_fec_protocol_bit_errors == 0
assert result.receive.crc_pass_count == 68
assert result.receive.crc_fail_count == 0
assert result.receive.object_complete
assert result.sha_match
def test_continuous_receiver_has_no_tx_oracle_parameters(monkeypatch) -> None:
obj, stream_plan = _small_continuous_stream(leading=5)
parameters = set(inspect.signature(lab046.scan_continuous_soft_stream).parameters)
forbidden = {
"frame_plans",
"frame_starts",
"frame_count",
"fragment_indexes",
"packets",
"coded_bits",
"original_object",
}
assert parameters.isdisjoint(forbidden)
def forbidden_tx_lookup(*args, **kwargs):
raise AssertionError("scanner обратился к TX-side plan")
monkeypatch.setattr(lab046, "build_object_frames", forbidden_tx_lookup)
received = lab046.scan_continuous_soft_stream(stream_plan.soft_symbols)
assert received.recovered_object == obj