Lab047: transmit JPEG over real RF link
This commit is contained in:
@@ -54,8 +54,14 @@ BACKGROUND_1400MHZ_LOCAL_PEAK_QUANTILE = 0.999
|
||||
BACKGROUND_1400MHZ_MAD_MULTIPLIER = 4.0
|
||||
FIXED_1400MHZ_CANDIDATE_THRESHOLD = 0.03361873421818018
|
||||
DETECTION_CHUNK_SAMPLES = 16_000_000
|
||||
MAX_CANDIDATES = 10_000
|
||||
MAX_SEARCH_ITERATIONS = 10_000
|
||||
# В потоковом проходе это предел реально выполненных строгих проверок, а не
|
||||
# размер промежуточного списка. Он не зависит от ожидаемого числа кадров.
|
||||
MAX_CANDIDATES = 100_000
|
||||
MAX_STORED_COARSE_CANDIDATES = 10_000
|
||||
MAX_STORED_OUTCOMES = 4_096
|
||||
MAX_STORED_CURSOR_POSITIONS = MAX_STORED_OUTCOMES + 1
|
||||
COARSE_CANDIDATE_MEMORY_BYTES = 108
|
||||
MAX_SEARCH_ITERATIONS = MAX_CANDIDATES
|
||||
MAX_VALID_FRAMES = 256
|
||||
REFINEMENT_MARGIN_SAMPLES = 4_000
|
||||
# resample_poly/fftconvolve одновременно создают несколько крупных временных
|
||||
@@ -143,6 +149,12 @@ class CoarseSearchResult:
|
||||
elapsed_seconds: float
|
||||
termination_reason: str
|
||||
max_candidates_reached: bool
|
||||
candidate_count: int = 0
|
||||
candidates_skipped_as_same_local_event: int = 0
|
||||
exact_duplicate_candidates_skipped: int = 0
|
||||
blocks_processed: int = 0
|
||||
processed_until_sample: int = 0
|
||||
candidate_memory_bytes_peak: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -197,6 +209,8 @@ class StrictSearchResult:
|
||||
termination_reason: str
|
||||
max_search_iterations_reached: bool
|
||||
max_valid_frames_reached: bool
|
||||
outcomes_truncated: bool = False
|
||||
cursor_history_truncated: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -524,7 +538,7 @@ def coarse_search_capture(
|
||||
capture_path: Path,
|
||||
rule: CandidateRule,
|
||||
*,
|
||||
max_candidates: int = MAX_CANDIDATES,
|
||||
max_candidates: int = MAX_STORED_COARSE_CANDIDATES,
|
||||
chunk_samples: int = DETECTION_CHUNK_SAMPLES,
|
||||
) -> CoarseSearchResult:
|
||||
"""Найти first-stage candidates во всём capture без TX offsets/count."""
|
||||
@@ -538,6 +552,8 @@ def coarse_search_capture(
|
||||
total_local_peaks = 0
|
||||
maximum_window = 0
|
||||
maximum_temporary = 0
|
||||
blocks_processed = 0
|
||||
processed_until_sample = 0
|
||||
termination = "END_OF_CAPTURE"
|
||||
max_reached = False
|
||||
decimation = lab046c.lab044.lab043.DETECTION_DECIMATION
|
||||
@@ -546,6 +562,11 @@ def coarse_search_capture(
|
||||
whole,
|
||||
chunk_samples=chunk_samples,
|
||||
):
|
||||
blocks_processed += 1
|
||||
processed_until_sample = min(
|
||||
sample_count,
|
||||
window_start + int(scores.size) * decimation,
|
||||
)
|
||||
total_local_peaks += int(indexes.size)
|
||||
maximum_window = max(maximum_window, window_samples)
|
||||
maximum_temporary = max(maximum_temporary, temporary_bytes)
|
||||
@@ -571,6 +592,10 @@ def coarse_search_capture(
|
||||
elapsed_seconds=time.perf_counter() - started,
|
||||
termination_reason=termination,
|
||||
max_candidates_reached=max_reached,
|
||||
candidate_count=len(candidates),
|
||||
blocks_processed=blocks_processed,
|
||||
processed_until_sample=processed_until_sample,
|
||||
candidate_memory_bytes_peak=len(candidates) * COARSE_CANDIDATE_MEMORY_BYTES,
|
||||
)
|
||||
|
||||
|
||||
@@ -872,6 +897,216 @@ def strict_process_candidates(
|
||||
)
|
||||
|
||||
|
||||
def stream_process_capture(
|
||||
capture_path: Path,
|
||||
rule: CandidateRule,
|
||||
*,
|
||||
max_candidates: int = MAX_CANDIDATES,
|
||||
max_valid_frames: int = MAX_VALID_FRAMES,
|
||||
chunk_samples: int = DETECTION_CHUNK_SAMPLES,
|
||||
decoder: CandidateDecoder = decode_candidate_from_memmap,
|
||||
) -> tuple[CoarseSearchResult, StrictSearchResult]:
|
||||
"""Последовательно находить и сразу строго проверять кандидатов.
|
||||
|
||||
``max_candidates`` ограничивает число вызовов строгого декодера за весь
|
||||
capture. Кандидаты всего файла не накапливаются. После CRC PASS курсор
|
||||
владеет областью до конца принятого кадра; после отказа пропускается только
|
||||
текущая позиция. Точный повтор sample offset подавляется без радиуса,
|
||||
который мог бы скрыть соседний настоящий кадр.
|
||||
"""
|
||||
|
||||
if max_candidates < 0 or max_valid_frames < 0:
|
||||
raise ValueError("Safety limits не могут быть отрицательными")
|
||||
raw, sample_count = _validate_capture(capture_path)
|
||||
whole = SampleInterval("FULL_CAPTURE", 0, sample_count)
|
||||
started = time.perf_counter()
|
||||
decimation = lab046c.lab044.lab043.DETECTION_DECIMATION
|
||||
|
||||
total_local_peaks = 0
|
||||
first_stage_candidates = 0
|
||||
candidates_skipped_as_same_local_event = 0
|
||||
exact_duplicate_candidates_skipped = 0
|
||||
blocks_processed = 0
|
||||
processed_until_sample = 0
|
||||
maximum_window = 0
|
||||
maximum_temporary = 0
|
||||
candidate_memory_peak = 0
|
||||
last_candidate_offset: int | None = None
|
||||
|
||||
cursor = 0
|
||||
cursor_history = [0]
|
||||
cursor_history_truncated = False
|
||||
stored_outcomes: list[CandidateOutcome] = []
|
||||
outcomes_truncated = False
|
||||
accepted_frames: list[lab046b.IqFrameReception] = []
|
||||
strict_elapsed = 0.0
|
||||
maximum_strict_window = 0
|
||||
counters = {
|
||||
"candidates_processed": 0,
|
||||
"rejected_during_refinement": 0,
|
||||
"refinement_pass": 0,
|
||||
"timing_pass": 0,
|
||||
"carrier_pass": 0,
|
||||
"phy_header_pass": 0,
|
||||
"phy_header_fail": 0,
|
||||
"fec_attempts": 0,
|
||||
"fec_pass": 0,
|
||||
"fec_fail": 0,
|
||||
"protocol_parse_pass": 0,
|
||||
"crc_pass": 0,
|
||||
"crc_fail": 0,
|
||||
"valid_frames": 0,
|
||||
}
|
||||
termination = "END_OF_CAPTURE"
|
||||
max_candidates_reached = False
|
||||
max_valid_frames_reached = False
|
||||
stop = False
|
||||
|
||||
for window_start, scores, indexes, window_samples, temporary_bytes in _score_blocks(
|
||||
raw,
|
||||
whole,
|
||||
chunk_samples=chunk_samples,
|
||||
):
|
||||
blocks_processed += 1
|
||||
processed_until_sample = min(
|
||||
sample_count,
|
||||
window_start + int(scores.size) * decimation,
|
||||
)
|
||||
total_local_peaks += int(indexes.size)
|
||||
maximum_window = max(maximum_window, window_samples)
|
||||
selected = indexes[scores[indexes] >= np.float32(rule.threshold)]
|
||||
maximum_temporary = max(
|
||||
maximum_temporary,
|
||||
temporary_bytes + int(selected.nbytes) + COARSE_CANDIDATE_MEMORY_BYTES,
|
||||
)
|
||||
candidate_memory_peak = max(
|
||||
candidate_memory_peak,
|
||||
int(selected.nbytes) + COARSE_CANDIDATE_MEMORY_BYTES,
|
||||
)
|
||||
for index in selected:
|
||||
sample_offset = window_start + int(index) * decimation
|
||||
if last_candidate_offset is not None:
|
||||
if sample_offset < last_candidate_offset:
|
||||
raise RuntimeError("Кандидаты потокового поиска нарушили порядок offset")
|
||||
if sample_offset == last_candidate_offset:
|
||||
exact_duplicate_candidates_skipped += 1
|
||||
continue
|
||||
last_candidate_offset = sample_offset
|
||||
first_stage_candidates += 1
|
||||
if sample_offset < cursor:
|
||||
candidates_skipped_as_same_local_event += 1
|
||||
continue
|
||||
if counters["candidates_processed"] >= max_candidates:
|
||||
termination = "MAX_CANDIDATES_REACHED"
|
||||
max_candidates_reached = True
|
||||
stop = True
|
||||
break
|
||||
if len(accepted_frames) >= max_valid_frames:
|
||||
termination = "MAX_VALID_FRAMES_REACHED"
|
||||
max_valid_frames_reached = True
|
||||
stop = True
|
||||
break
|
||||
|
||||
candidate = CoarseCandidate(
|
||||
sample_offset=sample_offset,
|
||||
score=float(scores[index]),
|
||||
)
|
||||
outcome = decoder(
|
||||
raw,
|
||||
sample_count,
|
||||
candidate,
|
||||
counters["candidates_processed"] + 1,
|
||||
)
|
||||
counters["candidates_processed"] += 1
|
||||
counters["rejected_during_refinement"] += int(not outcome.refinement_pass)
|
||||
counters["refinement_pass"] += int(outcome.refinement_pass)
|
||||
counters["timing_pass"] += int(outcome.timing_pass)
|
||||
counters["carrier_pass"] += int(outcome.carrier_pass)
|
||||
counters["phy_header_pass"] += int(outcome.phy_header_pass)
|
||||
counters["phy_header_fail"] += int(
|
||||
outcome.phy_header_attempted and not outcome.phy_header_pass
|
||||
)
|
||||
counters["fec_attempts"] += int(outcome.fec_attempted)
|
||||
counters["fec_pass"] += int(outcome.fec_pass)
|
||||
counters["fec_fail"] += int(outcome.fec_attempted and not outcome.fec_pass)
|
||||
counters["protocol_parse_pass"] += int(outcome.protocol_parse_pass)
|
||||
counters["crc_pass"] += int(outcome.crc == lab046c.lab045.CRC_PASS)
|
||||
counters["crc_fail"] += int(outcome.crc == lab046c.lab045.CRC_FAIL)
|
||||
strict_elapsed += outcome.elapsed_seconds
|
||||
maximum_strict_window = max(
|
||||
maximum_strict_window,
|
||||
outcome.maximum_local_window_samples,
|
||||
)
|
||||
|
||||
accepted = bool(
|
||||
outcome.accepted
|
||||
and outcome.frame is not None
|
||||
and outcome.crc == lab046c.lab045.CRC_PASS
|
||||
)
|
||||
if accepted:
|
||||
accepted_frames.append(outcome.frame)
|
||||
counters["valid_frames"] += 1
|
||||
if len(stored_outcomes) < MAX_STORED_OUTCOMES or accepted:
|
||||
stored_outcomes.append(outcome)
|
||||
else:
|
||||
outcomes_truncated = True
|
||||
|
||||
next_cursor = int(outcome.next_sample) if accepted else sample_offset + 1
|
||||
cursor = max(cursor + 1, next_cursor)
|
||||
if len(cursor_history) < MAX_STORED_CURSOR_POSITIONS:
|
||||
cursor_history.append(cursor)
|
||||
else:
|
||||
cursor_history_truncated = True
|
||||
if stop:
|
||||
break
|
||||
|
||||
# Если генератор блоков исчерпан без защитной остановки, обработан весь
|
||||
# интервал. Последний допустимый старт корреляции закономерно раньше конца
|
||||
# IQ на длину опорной последовательности и не является границей покрытия.
|
||||
if not stop:
|
||||
processed_until_sample = sample_count
|
||||
|
||||
total_elapsed = time.perf_counter() - started
|
||||
coarse = CoarseSearchResult(
|
||||
sample_count=sample_count,
|
||||
total_local_peaks=total_local_peaks,
|
||||
candidates=(),
|
||||
maximum_window_samples=maximum_window,
|
||||
estimated_maximum_temporary_bytes=maximum_temporary,
|
||||
elapsed_seconds=max(0.0, total_elapsed - strict_elapsed),
|
||||
termination_reason=termination,
|
||||
max_candidates_reached=max_candidates_reached,
|
||||
candidate_count=first_stage_candidates,
|
||||
candidates_skipped_as_same_local_event=candidates_skipped_as_same_local_event,
|
||||
exact_duplicate_candidates_skipped=exact_duplicate_candidates_skipped,
|
||||
blocks_processed=blocks_processed,
|
||||
processed_until_sample=processed_until_sample,
|
||||
candidate_memory_bytes_peak=candidate_memory_peak,
|
||||
)
|
||||
strict = StrictSearchResult(
|
||||
counters=StrictCounters(**counters),
|
||||
accepted_frames=tuple(accepted_frames),
|
||||
outcomes=tuple(stored_outcomes),
|
||||
cursor_history=tuple(cursor_history),
|
||||
elapsed_seconds=strict_elapsed,
|
||||
average_candidate_seconds=(
|
||||
strict_elapsed / counters["candidates_processed"]
|
||||
if counters["candidates_processed"]
|
||||
else 0.0
|
||||
),
|
||||
maximum_local_window_samples=maximum_strict_window,
|
||||
maximum_local_window_bytes=(
|
||||
maximum_strict_window * np.dtype(np.complex64).itemsize
|
||||
),
|
||||
termination_reason=termination,
|
||||
max_search_iterations_reached=max_candidates_reached,
|
||||
max_valid_frames_reached=max_valid_frames_reached,
|
||||
outcomes_truncated=outcomes_truncated,
|
||||
cursor_history_truncated=cursor_history_truncated,
|
||||
)
|
||||
return coarse, strict
|
||||
|
||||
|
||||
def assemble_independent_object(
|
||||
frames: tuple[lab046b.IqFrameReception, ...],
|
||||
) -> ObjectVerdict:
|
||||
@@ -914,24 +1149,15 @@ def _run_with_fixed_candidate_rule(
|
||||
|
||||
capture_path = Path(capture_path)
|
||||
_, sample_count = _validate_capture(capture_path)
|
||||
coarse = coarse_search_capture(capture_path, rule)
|
||||
if coarse.max_candidates_reached:
|
||||
strict = strict_process_candidates(
|
||||
capture_path,
|
||||
(),
|
||||
max_search_iterations=0,
|
||||
max_valid_frames=0,
|
||||
)
|
||||
verdict = assemble_independent_object(())
|
||||
else:
|
||||
strict = strict_process_candidates(capture_path, coarse.candidates)
|
||||
verdict = assemble_independent_object(strict.accepted_frames)
|
||||
coarse, strict = stream_process_capture(capture_path, rule)
|
||||
verdict = assemble_independent_object(strict.accepted_frames)
|
||||
elapsed = time.perf_counter() - full_started
|
||||
completed = bool(
|
||||
not coarse.max_candidates_reached
|
||||
and not strict.max_search_iterations_reached
|
||||
and not strict.max_valid_frames_reached
|
||||
and strict.termination_reason == "END_OF_CANDIDATES"
|
||||
and strict.termination_reason == "END_OF_CAPTURE"
|
||||
and coarse.processed_until_sample == sample_count
|
||||
)
|
||||
return IndependentRecoveryResult(
|
||||
completed=completed,
|
||||
@@ -1212,6 +1438,8 @@ def _independent_summary(result: IndependentRecoveryResult) -> dict:
|
||||
"termination_reason": result.strict.termination_reason,
|
||||
"max_search_iterations_reached": result.strict.max_search_iterations_reached,
|
||||
"max_valid_frames_reached": result.strict.max_valid_frames_reached,
|
||||
"outcomes_truncated": result.strict.outcomes_truncated,
|
||||
"cursor_history_truncated": result.strict.cursor_history_truncated,
|
||||
"outcomes": [
|
||||
{
|
||||
key: value
|
||||
|
||||
1411
experiments/lab047_jpeg_object_transport.py
Normal file
1411
experiments/lab047_jpeg_object_transport.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user