Lab052: suppress false preamble candidates

This commit is contained in:
LittleSam129
2026-09-08 17:29:52 +03:00
parent 9f33607d3a
commit cc38900cae
27 changed files with 77397 additions and 0 deletions

View File

@@ -1859,3 +1859,147 @@ RF TX=0; HackRF,180 МГц,QPSK,новый FEC/кодек и аппаратны
стоимости уникальных фоновых кандидатов в полном режиме5 изображений/с, стоимости уникальных фоновых кандидатов в полном режиме5 изображений/с,
с неизменными PHY/FEC/CRC и независимыми положительными/отрицательными регрессиями. с неизменными PHY/FEC/CRC и независимыми положительными/отрицательными регрессиями.
До отдельного разрешения не выполнять RF или аппаратные500 ксимв/с. До отдельного разрешения не выполнять RF или аппаратные500 ксимв/с.
# Запись 041
## Дата
8 сентября 2026 года
## Тема
Lab052 — уменьшение числа ложных кандидатов преамбулы при 500 ксимв/с,
сохранение слабых пакетов и повторная проверка свежести JPEG
## Выполнено
Перед новой разработкой проверены root/main/HEAD/status, diff stat/check,
фактический код Lab051, отчёт, индекс и запись 040. Повторные шлюзы Lab051:
quick_gate PASS, pytest 688 passed, 0 failed. Verifier подтвердил scope.
По прямому разрешению нового задания показан staged scope из 13 файлов,
создан коммит 9f33607d3ae82992c595de37dd64e99d7e4dea73:
Lab051: accelerate 500 ksymbol/s streaming receiver.
9695 добавлений; код, тесты, документация и шесть итоговых JSON Lab051.
IQ-окна, промежуточные результаты и посторонние файлы не добавлены; push не было.
Lab052 начата от этого фактического HEAD.
Рабочий режим сохранён: BPSK, 500000 символов/с, Fs=8000000, 16 отсчётов/символ,
JPEG C 128×96 RGB q25, 1740–1893 байта. Управление ELRS/CRSF остаётся отдельно.
Ни один файл protocol, старые модули PHY/FEC/CRC или формат фрагмента не изменён.
Уточнена исходная метрика: исторические 147082 Lab051 — все строгие кандидаты,
включая 240 valid; ложных было 146842. Новые timing-окна 12239 также включают
настоящие. Прежняя длинная сетка зависела от несохранённых encode_seconds.
В Lab052 тот же генератор с seed50050/CFO10/фазой0,73/чередованием полярности
использует один сохранённый замер готовности JPEG; SHA всех изображений проверяется.
Все сравниваемые варианты получают одну и ту же новую воспроизводимую сетку.
Точное историческое размещение пакетов не объявляется восстановленным.
Профиль прежнего Lab051 на 6,044416 с IQ:1675842 локальных пика,
392785 выше первого порога,147005 строгих проверок,240true/146765false.
24281,089 ложных проверок на секунду IQ. Ложные кандидаты дают11387 уникальных
уточнённых позиций и135378 повторных. Низкий нормированный порог пропускает шум
независимо от его абсолютной амплитуды; повторная PRBS7 создаёт сильные боковые пики.
Построены распределения нормированной корреляции, median/MAD и квантили по блокам.
Медиана false score0,035464; главный true peak0,841691. В357 блоках чистого фона,
выделенных только после независимых решений, медиана блочных медиан0,016353,
MAD0,006184; фон не идеально стационарен. Адаптивный median/MAD-порог рассмотрен,
но не выбран: он не устраняет структуру повторных пиков, а чувствительность
при смене фона требует отдельного доказательства. Нормировка энергией каждой
половины использует только наблюдаемый IQ, без TX reference.
Первый вариант подавлял меньшие пики в радиусе преамбулы и давал0false на основном
потоке, но adversary воспроизвёл потерю слабого VALID рядом с сильной ложной
преамбулой без правильного заголовка. Этот вариант отвергнут. Исправление:
сначала общие дешёвые корреляции всех локальных кандидатов по частям, затем
ограниченная полная ширина группы; FAIL представителя сохраняет альтернативы.
Прежний контрпример и его перенос через границы блоков больше не теряют пакет.
Основное правило: обе половины преамбулы проходят нормированную корреляцию
не ниже0,08474038063392896. Порог выбран заранее из номинальной модели
Beta(1,N−1),N640,alpha0,01; точная вероятность после фильтрации/отбора пиков
не утверждается. Две FFT частей общие на блок, per-candidate timing не запускается
на отклонённом шуме. Исходный первый порог0.030964473262429237 сохранён.
Фаза/полярность неизвестны, используется модуль; новый фазовый CFO-gate не введён.
Группа имеет полную ширину≤20480 отсчётов от первого пика, без цепочного расширения.
Минимальный допустимый кадр26720 отсчётов;20480<26720−2×2008=22704.
Это разделяет центральные кандидаты двух последовательных неперекрывающихся
кадров в диапазоне прежней точности локализации. Представитель — наибольшая
корреляция, tie→правый; при отказе проверяется следующая альтернатива.
Только настоящий PHY/FEC/CRC PASS разрешает пропуск принятого физического кадра.
Граница IQ-блока может добавить проверки, но не скрывает следующий кандидат.
Сравнены0/1/2/3 части. Ложных строгих проверок145863/332/15/6, во всех вариантах
сохранены те же240 уникальных уточнённых стартов и30 JPEG SHA MATCH.
Выбраны две части: минимальная проверка распределённой структуры,9784,33×
сокращение false. Третья часть уменьшает15до6, но дороже общего этапа.
Группировка без дешёвого фильтра и с корректным возвратом альтернатив почти
не снижает нагрузку распределённого шума. Глобальный peak/second не используется.
Обнаружены734 лишних построения неизменной опорной формы на каждый проход.
Введены локальные постоянные кэши опоры,RRC taps,PRBS bits,171140 байт.
Массивы побитно сравнены с прежними, глобальные функции возвращаются в finally.
Сами PHY,16 samples/symbol,все1601 timing-offset,soft Viterbi и CRC не менялись.
Три чистых полных прохода:processing/RF1,257688/1,219199/1,219273,
в каждом240/240 пакетов,30/30 точных JPEG,15false,0 потерь. Итог — целиком
медианный третий проход:7,369795 с обработки /6,044416 с IQ=1,219273.
Свежий прежний Lab051 без наблюдателя на той же сетке41,633312 с,ratio6,887897;
полное ускорение5,649×, относительно исторической Lab051 —5,242×.
Итоговые счётчики:392785 первого этапа,388407 отклонены дешёво,4378 сохранены,
384 группы,243 строгих представителя+12 альтернатив,15false/240true.
4123 кандидата пропущены внутри уже принятого физического кадра.
Строгих проверок42,187699/с IQ,ложных2,481629/с; новых soft-окон488.
Настоящие строгие проверки3,996005 с,среднее16,650 мс; ложные всего28,884 мс.
Общая децимация/корреляция1,790240 с,проверка частей0,446616 с.
INPUT5fps/30images;RECEIVED30;STALE30;DISPLAYED=FRESH DISPLAYED=0;FRESH FPS=0.
Возраст декодированных, затем отброшенных JPEG234,520/834,077/1549,595 мс.
MAX_VIDEO_AGE200 мс. Реальная измеренная стоимость каждого push и сборки/decode
использована в прежней событийной шкале. Нулевая стоимость RX не подставлялась.
Цель реального времени и5 свежих кадров/с НЕ достигнута. Теперь ограничение —
настоящие пакеты вместе с децимацией,FFT и копированием IQ,а не ложные кандидаты.
Финальный отдельный cProfile:FFT1,650 с,FIR1,295 с,Viterbi1,397 с собственного
времени. Вложенные matched/refine не суммируются повторно.
Камера/экран UNKNOWN;capture lookup применяется после JPEG только для отчётной
модели вывода. В неизменном fragment format нет capture timestamp, независимая
live-проверка возраста не реализована. Отставание1,426779 с соответствовало бы
91313848 байтам внешнего IQ backlog. Такой буфер не хранится офлайн-источником;
bounded state не объявляется готовой live-очередью без overflow/gap policy.
Пик удерживаемого IQ3177728 байт(3,031 МиБ),проверяемый предел3398912 байт.
Инженерная оценка рабочих массивов136,161 МиБ,не универсальный предел C-аллокаций.
RSS выбранного процесса после потока149393408 байт,peak168804352(160,984 МиБ).
Короткий отдельный replay измерил6754 наблюдаемых ndarray(6681 владельцев),
37378942 суммарных байта владельцев иtracemalloc peak11586792 байта.
Число массивов — нижняя граница Python-наблюдения; tracing не входит в CPU benchmark.
Длинный тест2048 блоков,16,777216 с распределённого шума со сменой амплитуды:
4783206 пиков,1068038 кандидатов,одна строгая ложная проверка,0 принятых пакетов.
Во всех контрольных точках после прогрева IQ2653440 байт,децимация397216,
сохранённых кандидатов0. RSS после прогрева кэшей стабилизируется.
После finish IQ-буферы освобождены. Основной поток:не более23 eligible пиков
и3 групп на блок; история наблюдателя существует только вне рабочего RX.
45 слабых сценариев Eb/N0=0/1/2/3/7 дБ,CFO−100/+10/+100 Гц,триseed/масштаба:
старый и новый правильно приняли37/45,0 дополнительных потерь;8 не приняты обоими.
Старая Lab047-запись20 ксимв/с штатным Lab048:36/36,CRC PASS,JPEG SHA MATCH,
19 ошибок до FEC,0 после,0 байтовых ошибок. Аппаратных запусков не было.
30 целевых тестов PASS,verifier иadversary независимо подтвердили исправление
ложного подавления,границы блоков,плохую CRC,эквивалентность частей и finally.
ФинальныеJSON,выбор медианного прохода,UTF-8,ссылки и график проверены независимо.
quick_gate PASS:718 тестов,20 функциональных проверок,13 штатных пропусков.
Полный pytest:718 passed за77,15 с,0 failed,одно предупреждение .pytest_cache.
Новый helper наблюдения массивов отдельно выполнен;три модуля проверены
py_compile и run_functional_tests после его добавления. git diff --check PASS.
Обязательные регрессии в acceptance.json PASS;real-time/5-fps target явно FAIL.
Lab052 git add/commit иpush не выполнялись;индекс пуст. RF TX=0.
Следующий этап: STOP. Для отдельно согласованной Lab053 исследовать стоимость
настоящих пакетов, общей децимации/FFT/копирования IQ и задержку JPEG-пачки.
Не переходить автоматически к10fps,QPSK,180 МГц,аппаратным500 ксимв/с или RF.

View File

@@ -0,0 +1,27 @@
{
"checks": {
"median_whole_timeline": true,
"all_240_packets": true,
"all_30_exact_jpegs": true,
"same_240_unique_refined_starts": true,
"same_first_stage_counts": true,
"false_count_15": true,
"no_sensitivity_losses": true,
"bounded_long_iq": true,
"longer_than_main": true,
"long_noise_accepts_zero": true,
"finish_clears_iq_and_candidates": true,
"old20k_36_crc_and_sha": true,
"measured_cpu_sum": true,
"radio_protocol_files_unchanged": true
},
"required_regressions_pass": true,
"software_5fps_target_pass": false,
"real_time_capable": false,
"fresh_display_fps": 0.0,
"processing_per_rf_second": 1.2192732766011487,
"speedup_same_fixture": 5.649181959197251,
"false_candidate_reduction": 9784.333333333334,
"rf_tx": 0,
"protocol_check_command": "git status --porcelain --untracked-files=all -- protocol"
}

View File

@@ -0,0 +1,13 @@
{
"observed_array_objects": 6754,
"observed_owned_arrays": 6681,
"observed_owned_bytes_total": 37378942,
"tracemalloc_retained_bytes": 6461,
"tracemalloc_peak_bytes": 11586792,
"array_count_is_lower_bound": true,
"iq_seconds": 0.063184125,
"packets": 1,
"interpretation": "observed Python ndarray lower bound; tracked RX allocation peak; input IQ prepared before tracing",
"timing_not_used_as_benchmark": true,
"rf_tx": 0
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 239 KiB

View File

@@ -0,0 +1,74 @@
7786130 function calls (7783174 primitive calls) in 12.641 seconds
Ordered by: internal time
List reduced from 336 to 30 due to restriction <30>
ncalls tottime percall cumtime percall filename:lineno(function)
7779 2.401 0.000 2.401 0.000 {built-in method scipy.fft._duccfft.pyduccfft.c2c}
240 1.355 0.006 1.363 0.006 lab051_streaming_receiver.py:139(fast_viterbi)
1467 1.290 0.001 1.301 0.001 _upfirdn.py:91(apply_filter)
312654 1.160 0.000 2.039 0.000 numeric.py:2337(isclose)
738 0.665 0.001 12.616 0.017 lab050_streaming_receiver.py:133(push_iq_block)
974 0.519 0.001 2.622 0.003 bpsk_radio.py:1079(root_raised_cosine_taps)
734 0.515 0.001 3.251 0.004 lab044_hackrf_bpsk_packet.py:189(_scale_and_carrier)
2746 0.327 0.000 0.327 0.000 {method 'copy' of 'numpy.ndarray' objects}
1455 0.267 0.000 0.269 0.000 lab043_pluto_to_rtlsdr.py:516(pilot_prbs7)
1563270 0.251 0.000 0.360 0.000 numeric.py:2429(<genexpr>)
2593 0.228 0.000 3.133 0.001 _signaltools.py:485(_freq_domain_conv)
1934 0.160 0.000 0.160 0.000 {method 'cumsum' of 'numpy.ndarray' objects}
7779 0.155 0.000 0.261 0.000 helper.py:146(_fix_shape)
16722 0.153 0.000 0.153 0.000 {built-in method numpy.zeros}
1624470 0.152 0.000 0.163 0.000 {built-in method builtins.isinstance}
29235 0.144 0.000 0.149 0.000 {built-in method numpy.array}
313134 0.128 0.000 0.251 0.000 _ufunc_config.py:440(__enter__)
480 0.125 0.000 1.649 0.003 lab051_streaming_receiver.py:258(matched_window)
12394 0.113 0.000 0.113 0.000 {method 'reduce' of 'numpy.ufunc' objects}
480 0.095 0.000 2.613 0.005 lab051_streaming_receiver.py:287(extract)
734 0.093 0.000 0.093 0.000 {built-in method scipy.ndimage._nd_image.min_or_max_filter1d}
784660 0.076 0.000 0.076 0.000 {built-in method builtins.abs}
734 0.074 0.000 0.540 0.001 lab043_hackrf_bpsk.py:248(_normalized_sliding_correlation)
313134 0.074 0.000 0.074 0.000 {built-in method numpy._core._multiarray_umath._make_extobj}
313134 0.071 0.000 0.124 0.000 _ufunc_config.py:456(__exit__)
885 0.070 0.000 0.075 0.000 _arraypad_impl.py:86(_pad_simple)
480 0.070 0.000 0.210 0.000 lab043_hackrf_bpsk.py:530(_evaluate_symbols)
5894 0.061 0.000 0.061 0.000 {method 'reshape' of 'numpy.ndarray' objects}
734 0.060 0.000 11.916 0.016 lab052_candidate_detection.py:142(_scan)
734 0.057 0.000 2.708 0.004 lab043_pluto_to_rtlsdr.py:458(shape_frame_like_lab042)
7786130 function calls (7783174 primitive calls) in 12.641 seconds
Ordered by: cumulative time
List reduced from 336 to 30 due to restriction <30>
ncalls tottime percall cumtime percall filename:lineno(function)
738 0.665 0.001 12.616 0.017 lab050_streaming_receiver.py:133(push_iq_block)
734 0.060 0.000 11.916 0.016 lab052_candidate_detection.py:142(_scan)
240 0.022 0.000 5.697 0.024 lab046c_offline_recovery.py:637(decode_candidate_from_memmap)
734 0.004 0.000 3.548 0.005 lab051_streaming_receiver.py:185(__init__)
734 0.007 0.000 3.544 0.005 lab044_hackrf_bpsk_packet.py:257(_preamble_reference_samples)
734 0.515 0.001 3.251 0.004 lab044_hackrf_bpsk_packet.py:189(_scale_and_carrier)
2593 0.228 0.000 3.133 0.001 _signaltools.py:485(_freq_domain_conv)
7779 0.012 0.000 2.869 0.000 _backend.py:19(__ua_function__)
7779 0.020 0.000 2.847 0.000 _basic_backend.py:52(_execute_nD)
7779 0.030 0.000 2.824 0.000 basic.py:126(c2cn)
2188 0.036 0.000 2.806 0.001 _signaltools.py:589(fftconvolve)
734 0.057 0.000 2.708 0.004 lab043_pluto_to_rtlsdr.py:458(shape_frame_like_lab042)
974 0.519 0.001 2.622 0.003 bpsk_radio.py:1079(root_raised_cosine_taps)
480 0.095 0.000 2.613 0.005 lab051_streaming_receiver.py:287(extract)
7779 2.401 0.000 2.401 0.000 {built-in method scipy.fft._duccfft.pyduccfft.c2c}
734 0.025 0.000 2.057 0.003 lab052_candidate_detection.py:112(coarse_scores)
312654 1.160 0.000 2.039 0.000 numeric.py:2337(isclose)
5186 0.005 0.000 1.855 0.000 _basic_backend.py:113(fftn)
480 0.125 0.000 1.649 0.003 lab051_streaming_receiver.py:258(matched_window)
240 0.023 0.000 1.508 0.006 lab045_hackrf_bpsk_fec.py:476(analyze_post_preamble_soft)
240 0.052 0.000 1.497 0.006 lab051_streaming_receiver.py:222(refine)
734 0.030 0.000 1.415 0.002 lab051_streaming_receiver.py:113(get)
1467 0.003 0.000 1.383 0.001 lab051_streaming_receiver.py:120(filtered)
1467 0.026 0.000 1.379 0.001 _signaltools.py:3884(resample_poly)
240 1.355 0.006 1.363 0.006 lab051_streaming_receiver.py:139(fast_viterbi)
1467 0.005 0.000 1.331 0.001 _upfirdn.py:108(upfirdn)
1467 1.290 0.001 1.301 0.001 _upfirdn.py:91(apply_filter)
2593 0.002 0.000 0.999 0.000 _basic_backend.py:120(ifftn)
405 0.022 0.000 0.797 0.002 _signaltools.py:850(oaconvolve)
240 0.001 0.000 0.661 0.003 lab043_hackrf_bpsk.py:366(_matched_filter_taps)

View File

@@ -0,0 +1,115 @@
{
"blocks": 2048,
"iq_seconds": 16.777216,
"background_scales": [
0.002,
0.2,
20,
0.02
],
"accepted_noise_packets": 0,
"peak_retained_iq_bytes": 3177728,
"iq_limit_bytes": 3398912,
"retained_after_finish_bytes": 0,
"constant_template_bytes": 171140,
"candidate_state_after_finish": 0,
"peak_eligible_candidates": 1,
"peak_cluster_count": 1,
"detector": {
"total_local_peaks": 4783206,
"first_stage_candidates": 1068038,
"cheap_stage_checked": 1068038,
"cheap_stage_rejected": 1068037,
"clusters": 1,
"representative_checks": 1
},
"strict_candidates": 1,
"processing_seconds": 8.453774801455438,
"checkpoints": [
{
"block": 256,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 0,
"counter_keys": 5,
"rss_bytes": 137297920,
"peak_rss_bytes": 168501248
},
{
"block": 512,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 139948032,
"peak_rss_bytes": 168501248
},
{
"block": 768,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 140001280,
"peak_rss_bytes": 168501248
},
{
"block": 1024,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 140533760,
"peak_rss_bytes": 168501248
},
{
"block": 1280,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 140533760,
"peak_rss_bytes": 168501248
},
{
"block": 1536,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 140541952,
"peak_rss_bytes": 168501248
},
{
"block": 1792,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 140550144,
"peak_rss_bytes": 168501248
},
{
"block": 2048,
"iq_bytes": 2653440,
"decimated_bytes": 397216,
"retained_candidates": 0,
"peak_eligible_candidates": 1,
"counter_keys": 7,
"rss_bytes": 140550144,
"peak_rss_bytes": 168501248
}
],
"process_memory": {
"rss_bytes": 135602176,
"peak_rss_bytes": 168501248
},
"rf_tx": 0
}

View File

@@ -0,0 +1,74 @@
1173653 function calls (1170697 primitive calls) in 8.087 seconds
Ordered by: internal time
List reduced from 324 to 30 due to restriction <30>
ncalls tottime percall cumtime percall filename:lineno(function)
10251 1.650 0.000 1.650 0.000 {built-in method scipy.fft._duccfft.pyduccfft.c2c}
240 1.397 0.006 1.405 0.006 lab051_streaming_receiver.py:139(fast_viterbi)
1467 1.295 0.001 1.308 0.001 _upfirdn.py:91(apply_filter)
738 0.553 0.001 8.065 0.011 lab050_streaming_receiver.py:133(push_iq_block)
2701 0.313 0.000 0.313 0.000 {method 'copy' of 'numpy.ndarray' objects}
14414 0.167 0.000 0.167 0.000 {built-in method numpy.zeros}
3417 0.157 0.000 2.311 0.001 _signaltools.py:485(_freq_domain_conv)
734 0.152 0.000 7.483 0.010 lab052_candidate_detection.py:181(_scan)
2687 0.145 0.000 0.145 0.000 {method 'cumsum' of 'numpy.ndarray' objects}
11504 0.113 0.000 0.113 0.000 {method 'reduce' of 'numpy.ufunc' objects}
488 0.098 0.000 0.865 0.002 lab051_streaming_receiver.py:258(matched_window)
734 0.095 0.000 0.531 0.001 lab052_candidate_detection.py:125(split_scores)
35184 0.091 0.000 0.097 0.000 {built-in method numpy.array}
495 0.085 0.000 1.733 0.004 lab051_streaming_receiver.py:287(extract)
488 0.075 0.000 0.225 0.000 lab043_hackrf_bpsk.py:530(_evaluate_symbols)
734 0.069 0.000 7.321 0.010 lab052_candidate_detection.py:185(_scan_with_templates)
10251 0.068 0.000 0.222 0.000 helper.py:146(_fix_shape)
734 0.062 0.000 0.372 0.001 lab043_hackrf_bpsk.py:248(_normalized_sliding_correlation)
240 0.059 0.000 0.083 0.000 bit_interleaver.py:41(build_permutation)
972 0.052 0.000 0.055 0.000 _arraypad_impl.py:86(_pad_simple)
488 0.051 0.000 0.533 0.001 lab051_streaming_receiver.py:41(fast_preamble_timing)
734 0.049 0.000 1.457 0.002 lab051_streaming_receiver.py:113(get)
255 0.049 0.000 1.084 0.004 lab051_streaming_receiver.py:222(refine)
13668 0.045 0.000 0.107 0.000 helper.py:47(_init_nd_shape_and_axes)
488 0.043 0.000 0.119 0.000 lab043_hackrf_bpsk.py:454(_estimate_cfo_gain)
2933 0.040 0.000 2.010 0.001 _signaltools.py:589(fftconvolve)
31006 0.040 0.000 0.040 0.000 {built-in method numpy.asarray}
5366 0.038 0.000 0.038 0.000 {method 'reshape' of 'numpy.ndarray' objects}
10251 0.034 0.000 2.061 0.000 basic.py:126(c2cn)
1467 0.032 0.000 1.401 0.001 _signaltools.py:3884(resample_poly)
1173653 function calls (1170697 primitive calls) in 8.087 seconds
Ordered by: cumulative time
List reduced from 324 to 30 due to restriction <30>
ncalls tottime percall cumtime percall filename:lineno(function)
738 0.553 0.001 8.065 0.011 lab050_streaming_receiver.py:133(push_iq_block)
734 0.152 0.000 7.483 0.010 lab052_candidate_detection.py:181(_scan)
734 0.069 0.000 7.321 0.010 lab052_candidate_detection.py:185(_scan_with_templates)
255 0.026 0.000 4.417 0.017 lab046c_offline_recovery.py:637(decode_candidate_from_memmap)
3417 0.157 0.000 2.311 0.001 _signaltools.py:485(_freq_domain_conv)
10251 0.014 0.000 2.110 0.000 _backend.py:19(__ua_function__)
10251 0.021 0.000 2.086 0.000 _basic_backend.py:52(_execute_nD)
10251 0.034 0.000 2.061 0.000 basic.py:126(c2cn)
2933 0.040 0.000 2.010 0.001 _signaltools.py:589(fftconvolve)
734 0.025 0.000 1.932 0.003 lab052_candidate_detection.py:111(coarse_scores)
495 0.085 0.000 1.733 0.004 lab051_streaming_receiver.py:287(extract)
10251 1.650 0.000 1.650 0.000 {built-in method scipy.fft._duccfft.pyduccfft.c2c}
240 0.026 0.000 1.558 0.006 lab045_hackrf_bpsk_fec.py:476(analyze_post_preamble_soft)
734 0.049 0.000 1.457 0.002 lab051_streaming_receiver.py:113(get)
1467 0.004 0.000 1.406 0.001 lab051_streaming_receiver.py:120(filtered)
240 1.397 0.006 1.405 0.006 lab051_streaming_receiver.py:139(fast_viterbi)
1467 0.032 0.000 1.401 0.001 _signaltools.py:3884(resample_poly)
6834 0.005 0.000 1.360 0.000 _basic_backend.py:113(fftn)
1467 0.005 0.000 1.342 0.001 _upfirdn.py:108(upfirdn)
1467 1.295 0.001 1.308 0.001 _upfirdn.py:91(apply_filter)
255 0.049 0.000 1.084 0.004 lab051_streaming_receiver.py:222(refine)
488 0.098 0.000 0.865 0.002 lab051_streaming_receiver.py:258(matched_window)
3417 0.002 0.000 0.733 0.000 _basic_backend.py:120(ifftn)
484 0.022 0.000 0.712 0.001 _signaltools.py:850(oaconvolve)
488 0.002 0.000 0.535 0.001 lab051_streaming_receiver.py:283(timing)
488 0.051 0.000 0.533 0.001 lab051_streaming_receiver.py:41(fast_preamble_timing)
734 0.095 0.000 0.531 0.001 lab052_candidate_detection.py:125(split_scores)
734 0.062 0.000 0.372 0.001 lab043_hackrf_bpsk.py:248(_normalized_sliding_correlation)
2701 0.313 0.000 0.313 0.000 {method 'copy' of 'numpy.ndarray' objects}
488 0.075 0.000 0.225 0.000 lab043_hackrf_bpsk.py:530(_evaluate_symbols)

View File

@@ -0,0 +1,198 @@
{
"stage": "LAB048_OFFLINE_RECEIVER_PERFORMANCE",
"baseline": {
"capture_seconds": 15.0,
"full_processing_seconds": 37.423910000012256,
"strict_stage_seconds": 32.45996900013415,
"average_strict_candidate_seconds": 0.47043433333527757,
"approximate_peak_bytes": 1545755352
},
"optimized": {
"capture_seconds": 15.0,
"full_processing_seconds": 13.213345700060017,
"coarse_search_seconds": 4.5940155000425875,
"strict_stage_seconds": 8.609627199941315,
"average_strict_candidate_seconds": 0.12477720579625094,
"approximate_peak_bytes": 1545755352,
"stage_measurements": {
"cs8_to_complex64": {
"calls": 113,
"cumulative_seconds": 1.3091708993306383
},
"refine_start": {
"calls": 69,
"cumulative_seconds": 2.8543391003040597
},
"matched_filter": {
"calls": 105,
"cumulative_seconds": 3.088835699832998
},
"timing": {
"calls": 105,
"cumulative_seconds": 1.2946644995827228
},
"cfo_phase_soft": {
"calls": 105,
"cumulative_seconds": 0.0416537004057318
},
"phy_header": {
"calls": 105,
"cumulative_seconds": 0.004346300149336457
},
"post_preamble_fec_crc": {
"calls": 36,
"cumulative_seconds": 0.6405917998636141
},
"viterbi": {
"calls": 36,
"cumulative_seconds": 0.6221252002287656
}
},
"memory_arrays": [
{
"name": "coarse_iq_window",
"dtype": "complex64",
"shape": [
16000000
],
"bytes": 128000000,
"mib": 122.0703125
},
{
"name": "coarse_decimated",
"dtype": "complex64",
"shape": [
800000
],
"bytes": 6400000,
"mib": 6.103515625
},
{
"name": "coarse_downconversion_vector",
"dtype": "complex64",
"shape": [
800000
],
"bytes": 6400000,
"mib": 6.103515625
},
{
"name": "coarse_correlation_scores",
"dtype": "float32",
"shape": [
773561
],
"bytes": 3094244,
"mib": 2.9509010314941406
},
{
"name": "strict_full_iq_window",
"dtype": "complex64",
"shape": [
2306400
],
"bytes": 18451200,
"mib": 17.596435546875
},
{
"name": "strict_cached_carrier_vector",
"dtype": "complex64",
"shape": [
2306400
],
"bytes": 18451200,
"mib": 17.596435546875
},
{
"name": "strict_baseband",
"dtype": "complex64",
"shape": [
2306400
],
"bytes": 18451200,
"mib": 17.596435546875
},
{
"name": "strict_matched_output",
"dtype": "complex64",
"shape": [
2310400
],
"bytes": 18483200,
"mib": 17.626953125
},
{
"name": "matched_filter_taps",
"dtype": "float32",
"shape": [
4001
],
"bytes": 16004,
"mib": 0.015262603759765625
}
]
},
"speedup": 2.8322811534282546,
"processing_to_capture_ratio": 0.8808897133373345,
"real_time_capable": true,
"threshold": 0.030964473262429237,
"threshold_changed": false,
"coarse": {
"total_local_peaks": 185828,
"first_stage_candidates": 32414,
"skipped_as_owned_event": 32345,
"exact_duplicates": 0
},
"strict": {
"candidates_processed": 69,
"rejected_during_refinement": 0,
"refinement_pass": 69,
"timing_pass": 69,
"carrier_pass": 69,
"phy_header_pass": 36,
"phy_header_fail": 33,
"fec_attempts": 36,
"fec_pass": 36,
"fec_fail": 0,
"protocol_parse_pass": 36,
"crc_pass": 36,
"crc_fail": 0,
"valid_frames": 36
},
"object": {
"valid_fragments": 36,
"missing_fragments": [],
"object_complete": true
},
"after_independent_verdict": {
"stage": "AFTER_COMPLETED_INDEPENDENT_VERDICT_ONLY",
"expected_tx_frames": 36,
"matched_valid_frames": 36,
"missed_tx_frames": [],
"false_crc_pass": 0,
"duplicate_valid_detections": 0,
"total_pre_fec_coded_bit_errors": 19,
"total_post_fec_protocol_bit_errors": 0,
"jpeg_size": 8724,
"recovered_sha256": "e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a",
"tx_sha256": "e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a",
"sha_match": true,
"byte_errors": 0,
"bit_errors": 0,
"jpeg_decode": "PASS",
"width": 320,
"height": 240
},
"receiver_oracles": {
"expected_frame_count": false,
"tx_coded_bits": false,
"tx_fragment_indexes": false,
"tx_frame_offsets": false,
"tx_image": false,
"tx_packets": false
},
"phy_changed": false,
"fec_changed": false,
"bpsk_changed": false,
"rf_tx": 0
}

View File

@@ -0,0 +1,124 @@
{
"rules": [
{
"rule": {
"parts": 0,
"radius_samples": 20480,
"part_threshold": null
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": 263.9658273381295,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 390639,
"clusters": 900,
"cheap_stage_checked": 796,
"inside_accepted_frame": 104,
"strict_true_candidates": 240,
"strict_false_candidates": 556,
"strict_checks_per_rf_second": 131.691796196688,
"cheap_seconds": 0.0009583993814885616,
"cluster_seconds": 0.15876410226337612,
"peak_cluster_count": 7,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1243896
},
"processing_per_rf_second": 2.6724659422897794,
"fresh_display_fps": 0.0
},
{
"rule": {
"parts": 1,
"radius_samples": 20480,
"part_threshold": 0.05995104488778001
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": 1298.8053097345132,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 390639,
"clusters": 900,
"cheap_stage_checked": 796,
"inside_accepted_frame": 104,
"cheap_stage_rejected": 443,
"strict_true_candidates": 240,
"strict_false_candidates": 113,
"strict_checks_per_rf_second": 58.40101012240058,
"cheap_seconds": 0.03248489915858954,
"cluster_seconds": 0.16109760117251426,
"peak_cluster_count": 7,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1243896
},
"processing_per_rf_second": 2.059989517566315,
"fresh_display_fps": 0.0
},
{
"rule": {
"parts": 2,
"radius_samples": 20480,
"part_threshold": 0.08474038063392896
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": null,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 390639,
"clusters": 900,
"cheap_stage_checked": 796,
"inside_accepted_frame": 104,
"cheap_stage_rejected": 556,
"strict_true_candidates": 240,
"strict_false_candidates": 0,
"strict_checks_per_rf_second": 39.706069205031554,
"cheap_seconds": 0.035114401835016906,
"cluster_seconds": 0.16016869980376214,
"peak_cluster_count": 7,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1243896
},
"processing_per_rf_second": 1.8833212008846116,
"fresh_display_fps": 0.0,
"false_candidate_reduction_fraction": 1.0
},
{
"rule": {
"parts": 3,
"radius_samples": 20480,
"part_threshold": 0.10381329338988134
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": null,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 390639,
"clusters": 900,
"cheap_stage_checked": 796,
"inside_accepted_frame": 104,
"cheap_stage_rejected": 556,
"strict_true_candidates": 240,
"strict_false_candidates": 0,
"strict_checks_per_rf_second": 39.706069205031554,
"cheap_seconds": 0.03812550043221563,
"cluster_seconds": 0.16675639932509512,
"peak_cluster_count": 7,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1243896
},
"processing_per_rf_second": 1.8967992111861114,
"fresh_display_fps": 0.0,
"false_candidate_reduction_fraction": 1.0
}
],
"rf_tx": 0,
"status": "REJECTED: strong invalid preamble suppresses neighboring valid packet",
"timing_includes_observer": true
}

View File

@@ -0,0 +1,121 @@
{
"rules": [
{
"rule": {
"parts": 0,
"width_samples": 20480,
"part_threshold": null
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": 1.0061838848782763,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 392785,
"cheap_stage_checked": 392785,
"cheap_stage_rejected": 0,
"clusters": 2777,
"inside_accepted_frame": 246682,
"strict_true_candidates": 240,
"strict_false_candidates": 145863,
"strict_checks_per_rf_second": 24171.56595442802,
"cheap_seconds": 0.0041361990151926875,
"cluster_seconds": 0.009281201055273414,
"peak_cluster_count": 15,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1236912
},
"processing_per_rf_second": 5.978309517500444,
"fresh_display_fps": 0.0
},
{
"rule": {
"parts": 1,
"width_samples": 20480,
"part_threshold": 0.05995104488778001
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": 442.06325301204816,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 392785,
"cheap_stage_checked": 392785,
"cheap_stage_rejected": 366683,
"clusters": 1354,
"inside_accepted_frame": 25530,
"strict_true_candidates": 240,
"strict_false_candidates": 332,
"strict_checks_per_rf_second": 94.63279827199187,
"cheap_seconds": 0.32703390275128186,
"cluster_seconds": 0.005597901064902544,
"peak_cluster_count": 4,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1330708
},
"processing_per_rf_second": 1.5535515426894586,
"fresh_display_fps": 0.0
},
{
"rule": {
"parts": 2,
"width_samples": 20480,
"part_threshold": 0.08474038063392896
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": 9784.333333333334,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 392785,
"cheap_stage_checked": 392785,
"cheap_stage_rejected": 388407,
"clusters": 384,
"inside_accepted_frame": 4123,
"strict_true_candidates": 240,
"strict_false_candidates": 15,
"strict_checks_per_rf_second": 42.187698530346026,
"cheap_seconds": 0.530722099239938,
"cluster_seconds": 0.0033054009545594454,
"peak_cluster_count": 3,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1480664
},
"processing_per_rf_second": 1.3386680530144077,
"fresh_display_fps": 0.0
},
{
"rule": {
"parts": 3,
"width_samples": 20480,
"part_threshold": 0.10381329338988134
},
"received_images": 30,
"object_jpeg_integrity": true,
"true_candidates_lost": 0,
"false_candidate_reduction": 24460.833333333332,
"detector": {
"total_local_peaks": 1675842,
"first_stage_candidates": 392785,
"cheap_stage_checked": 392785,
"cheap_stage_rejected": 390636,
"clusters": 281,
"inside_accepted_frame": 1903,
"strict_true_candidates": 240,
"strict_false_candidates": 6,
"strict_checks_per_rf_second": 40.69872093515734,
"cheap_seconds": 0.8188061001710594,
"cluster_seconds": 0.0032444989774376154,
"peak_cluster_count": 2,
"candidate_state_after_finish": 0,
"detection_arrays_peak_estimate_bytes": 1630812
},
"processing_per_rf_second": 1.410843181335602,
"fresh_display_fps": 0.0
}
],
"rf_tx": 0
}

View File

@@ -0,0 +1,905 @@
{
"cases": [
{
"ebn0_db": 0,
"cfo_hz": -100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 1045,
"CRC_FAIL": 31
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"CRC_FAIL": 2,
"PHY_HEADER_MAGIC_MISMATCH": 8
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": -100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 71
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": -100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 1029,
"CRC_NOT_REACHED": 11,
"TRUNCATED_SYMBOLS": 20
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"CRC_NOT_REACHED": 2,
"PHY_HEADER_MAGIC_MISMATCH": 10
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": 10,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 1115,
"PHY_HEADER_CHECKSUM_MISMATCH": 33,
"TRUNCATED_SYMBOLS": 10
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_CHECKSUM_MISMATCH": 1,
"PHY_HEADER_MAGIC_MISMATCH": 8
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": 10,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 974,
"PHY_HEADER_CHECKSUM_MISMATCH": 14,
"TRUNCATED_SYMBOLS": 25
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_CHECKSUM_MISMATCH": 2,
"PHY_HEADER_MAGIC_MISMATCH": 10
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": 10,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 1046,
"PHY_HEADER_CHECKSUM_MISMATCH": 14,
"TRUNCATED_SYMBOLS": 13
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_CHECKSUM_MISMATCH": 3,
"PHY_HEADER_MAGIC_MISMATCH": 11
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": 100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 1034,
"TRUNCATED_SYMBOLS": 52
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 9
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": 100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 77
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 0,
"cfo_hz": 100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 967,
"TRUNCATED_SYMBOLS": 18
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 11
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": -100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"TRUNCATED_SYMBOLS": 1,
"PHY_HEADER_MAGIC_MISMATCH": 98
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": -100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 65
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": -100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 76
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": 10,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"TRUNCATED_SYMBOLS": 1,
"PHY_HEADER_MAGIC_MISMATCH": 101
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": 10,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 1009,
"PHY_HEADER_CHECKSUM_MISMATCH": 13,
"TRUNCATED_SYMBOLS": 1
}
},
"optimized": {
"accepted": 0,
"payload_match": false,
"reasons": {
"PHY_HEADER_CHECKSUM_MISMATCH": 3,
"PHY_HEADER_MAGIC_MISMATCH": 11
}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": 10,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 75
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": 100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"TRUNCATED_SYMBOLS": 1,
"PHY_HEADER_MAGIC_MISMATCH": 100
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": 100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 67
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 1,
"cfo_hz": 100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 74
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": -100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"TRUNCATED_SYMBOLS": 1,
"PHY_HEADER_MAGIC_MISMATCH": 79
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": -100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 56
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": -100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 68
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": 10,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"TRUNCATED_SYMBOLS": 1,
"PHY_HEADER_MAGIC_MISMATCH": 83
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": 10,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 58
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": 10,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 70
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": 100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"TRUNCATED_SYMBOLS": 1,
"PHY_HEADER_MAGIC_MISMATCH": 85
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": 100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 57
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 2,
"cfo_hz": 100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 67
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": -100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 71
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": -100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 49
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": -100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 64
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": 10,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 74
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": 10,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 53
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": 10,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 64
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": 100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 76
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": 100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 53
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 3,
"cfo_hz": 100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 63
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": -100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 50
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": -100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 42
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": -100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 51
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": 10,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 50
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": 10,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 42
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": 10,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 51
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": 100,
"seed": 52031,
"amplitude_scale": 0.002,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 48
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": 100,
"seed": 52032,
"amplitude_scale": 0.2,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 42
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
},
{
"ebn0_db": 7,
"cfo_hz": 100,
"seed": 52033,
"amplitude_scale": 1.0,
"baseline": {
"accepted": 1,
"payload_match": true,
"reasons": {
"PHY_HEADER_MAGIC_MISMATCH": 44
}
},
"optimized": {
"accepted": 1,
"payload_match": true,
"reasons": {}
},
"lost_baseline_packet": false
}
],
"count": 45,
"baseline_correct": 37,
"optimized_correct": 37,
"lost_baseline_packets": 0,
"rf_tx": 0
}

View File

@@ -0,0 +1,186 @@
{
"seed": 50050,
"description": "Lab051 generator; frozen measured JPEG readiness",
"images": [
{
"image_id": 1,
"bytes": 1740,
"encode_seconds": 0.00024159997701644897,
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d"
},
{
"image_id": 2,
"bytes": 1790,
"encode_seconds": 0.000175100052729249,
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85"
},
{
"image_id": 3,
"bytes": 1821,
"encode_seconds": 0.00018400000408291817,
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced"
},
{
"image_id": 4,
"bytes": 1844,
"encode_seconds": 0.0001865000231191516,
"sha256": "38052148ffbc024d2ba28049ea20b1172eebe9c990cb2fd94b25d2eaf9b46584"
},
{
"image_id": 5,
"bytes": 1871,
"encode_seconds": 0.00015390000771731138,
"sha256": "1e135c2854bef0f7dffb84cf8eb71e200c958d7d7426d24d16bb36d471b52c1d"
},
{
"image_id": 6,
"bytes": 1889,
"encode_seconds": 0.00016799999866634607,
"sha256": "b59b7e834374fa00757abcb57282a1b7c46c08b41e0adb1cb5803135b009cb7f"
},
{
"image_id": 7,
"bytes": 1893,
"encode_seconds": 0.00022439996246248484,
"sha256": "8f6452262ea5a82d195de8cd23e3d21ab186ce5a20b031acf8295c30daa98572"
},
{
"image_id": 8,
"bytes": 1877,
"encode_seconds": 0.00017830007709562778,
"sha256": "eb0f69a3e57c1860d98e79591ee9b472ac3a0929883be2a2992397f594d176f4"
},
{
"image_id": 9,
"bytes": 1844,
"encode_seconds": 0.00019099994096904993,
"sha256": "22c41e53d1d086baf886e6f65c011e1360ef26ad7781eced4655664cb7adcf06"
},
{
"image_id": 10,
"bytes": 1826,
"encode_seconds": 0.00017690006643533707,
"sha256": "e907decbbf66cc21c6b6e767b6c9baef856754d3cc95625cca38fd07aa5408c1"
},
{
"image_id": 11,
"bytes": 1878,
"encode_seconds": 0.00019090005662292242,
"sha256": "ab681a9cce96a8aaa71f3a612634748a2df743f1c76092f5a674e0dd36ca7caf"
},
{
"image_id": 12,
"bytes": 1805,
"encode_seconds": 0.00017190002836287022,
"sha256": "9c3d4258ddf1085f8da6ca87b6c3071aa031d58a5c42739a5527a27e6c2f17af"
},
{
"image_id": 13,
"bytes": 1819,
"encode_seconds": 0.0001813999842852354,
"sha256": "6615eb4fe58fb4d0854c3c856e65a0990eba2361fc977c524acf9f0a5ab7accb"
},
{
"image_id": 14,
"bytes": 1866,
"encode_seconds": 0.0002503000432625413,
"sha256": "8fbb4fa0fce1e3deedb3fd838d69835b7670d73d793b792b2ffde69d733bbb40"
},
{
"image_id": 15,
"bytes": 1892,
"encode_seconds": 0.00020739994943141937,
"sha256": "9109553288f7b07311be951bd36cc389034496b6665b874dd92cfea84ac60d13"
},
{
"image_id": 16,
"bytes": 1886,
"encode_seconds": 0.00019369996152818203,
"sha256": "5b264bae760e7103f645f4a4203c435dfb8bd7555ec679b750d4c4cefe642f3a"
},
{
"image_id": 17,
"bytes": 1881,
"encode_seconds": 0.00017360004130750895,
"sha256": "df7d2804878be2b11c0233cee2401504d3545fad2f856fedb5e3aedfc1a0bf36"
},
{
"image_id": 18,
"bytes": 1862,
"encode_seconds": 0.00019259995315223932,
"sha256": "9fc4dded148ed750d53cadf0abe059cb01acab50999ebf9f521b2685c77c3025"
},
{
"image_id": 19,
"bytes": 1838,
"encode_seconds": 0.00017380004283040762,
"sha256": "820d8d0916c53b1937d002e6b0632100b7a2d0a216f1022cdc55ba5fd0844cb5"
},
{
"image_id": 20,
"bytes": 1859,
"encode_seconds": 0.0001799999736249447,
"sha256": "ef9903b6c8d9ab6f572a7fa54af7570f6c321346117f9c4e156e1b71c97c46fd"
},
{
"image_id": 21,
"bytes": 1892,
"encode_seconds": 0.00019259995315223932,
"sha256": "6605b523265e1229959f94fdad35d672037a902e9c7c585e22aafb875fb9e287"
},
{
"image_id": 22,
"bytes": 1783,
"encode_seconds": 0.00022350007202476263,
"sha256": "8cf09da5a66ca3b3b18c1e4db63c8d57ed5044ba6f758368f7bcfb04f86a3839"
},
{
"image_id": 23,
"bytes": 1826,
"encode_seconds": 0.0001976999919861555,
"sha256": "282ee75b1d6c2963fec0ecaed77f5663d9a49bcfa1561c5b0f40ea93089e06f1"
},
{
"image_id": 24,
"bytes": 1870,
"encode_seconds": 0.00019099994096904993,
"sha256": "ba6c6a8aeae49674442d1079d15f4b0fa4202e7cc72cb66d2853c312ced16132"
},
{
"image_id": 25,
"bytes": 1872,
"encode_seconds": 0.0001868000254034996,
"sha256": "bd7841541d47d9a9844e6302825db98f3c72e5258946c3d2d0a2d2b7f0cee92e"
},
{
"image_id": 26,
"bytes": 1872,
"encode_seconds": 0.00018989993259310722,
"sha256": "70cdf837f756084e3d5a6dd438b615d71e669936c4d17c9ed7be88f4b4dcf965"
},
{
"image_id": 27,
"bytes": 1870,
"encode_seconds": 0.0001943999668583274,
"sha256": "a31da69de44bff031631997cc27724025f293528b4d97f2825d34a704ec224af"
},
{
"image_id": 28,
"bytes": 1848,
"encode_seconds": 0.00018319999799132347,
"sha256": "4380eae166bc37a7de7367642559b948cf1cc77939d1c84406de7cff372b6a76"
},
{
"image_id": 29,
"bytes": 1871,
"encode_seconds": 0.00016330007929354906,
"sha256": "c2e1a913dc48d622e1097f31543208ce610ce42d266857d8a76961f143dd9a4b"
},
{
"image_id": 30,
"bytes": 1869,
"encode_seconds": 0.0001724000321701169,
"sha256": "bbbd15eac04b06ea05ec548f4c95b577b8ffc280c00fc473fb2a6914c40f7939"
}
]
}

View File

@@ -0,0 +1,107 @@
{
"classification": "strict PHY/FEC/CRC verdict first; no diagnostic feedback into RX",
"baseline_true_unique_starts": 240,
"optimized_true_unique_starts": 240,
"missing_true_starts": [],
"unexpected_true_starts": [],
"true_main_peak_scores": {
"count": 240,
"min": 0.8300548791885376,
"median": 0.8416910767555237,
"mad": 0.0021253228187561035,
"p90": 0.8455655992031097,
"p99": 0.8483891654014587,
"p999": 0.8496067104935646,
"max": 0.8499454259872437
},
"false_strict_scores": {
"count": 146765,
"min": 0.03096448816359043,
"median": 0.03546393662691116,
"mad": 0.0029666051268577576,
"p90": 0.04409027695655823,
"p99": 0.0558026286959648,
"p999": 0.5325132031440795,
"max": 0.8059508800506592
},
"baseline_false_refined_positions": 11387,
"baseline_false_repeated_refinements": 135378,
"noise_only_blocks": 357,
"noise_block_statistics": {
"median": {
"count": 357,
"min": 0.006450756685808301,
"median": 0.016353150829672813,
"mad": 0.00045966170728206635,
"p90": 0.016945500299334525,
"p99": 0.017328095547854902,
"p999": 0.017409235779196026,
"max": 0.017421899363398552
},
"mad": {
"count": 357,
"min": 0.002472435589879751,
"median": 0.006183675490319729,
"mad": 0.0002206573262810707,
"p90": 0.006455647945404053,
"p99": 0.006683134622871876,
"p999": 0.006718997411429882,
"max": 0.006726322695612907
},
"p90": {
"count": 357,
"min": 0.011724081635475166,
"median": 0.029762874729931357,
"mad": 0.0008111353963613475,
"p90": 0.0307584797218442,
"p99": 0.03153721953928471,
"p999": 0.03181600074619056,
"max": 0.03183051086962224
},
"p99": {
"count": 357,
"min": 0.016392295099794868,
"median": 0.041875139921903615,
"mad": 0.001360640600323683,
"p90": 0.043799061246216305,
"p99": 0.04486697037369015,
"p999": 0.045995346079021705,
"max": 0.04610396962612869
},
"p999": {
"count": 357,
"min": 0.019085338560864334,
"median": 0.05054646224901104,
"mad": 0.002620417140424082,
"p90": 0.05440113909915092,
"p99": 0.05857364904597424,
"p999": 0.06542779265359067,
"max": 0.06649338830262444
}
},
"split_minimum_true_strict": {
"count": 240,
"min": 0.8250381350517273,
"median": 0.8373404443264008,
"mad": 0.0029025375843048096,
"p90": 0.8425169467926026,
"p99": 0.8459141814708709,
"p999": 0.8461976568698883,
"max": 0.8462259769439697
},
"split_minimum_other_candidates": {
"count": 392545,
"min": 3.158521940349601e-05,
"median": 0.02823474630713463,
"mad": 0.008037906140089035,
"p90": 0.04402520880103111,
"p99": 0.08912204891443251,
"p999": 0.662676521778107,
"max": 0.7688561081886292
},
"strict_false_reduction": 9784.333333333334,
"strict_false_reduction_fraction": 0.9998977957960004,
"first_stage_threshold_unchanged": 0.030964473262429237,
"part_threshold": 0.08474038063392896,
"rf_tx": 0
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -308,6 +308,46 @@
кандидатов в полном потоке 5 кадров/с с тем же PHY/FEC/CRC. кандидатов в полном потоке 5 кадров/с с тем же PHY/FEC/CRC.
- **RF / Git:** RF TX=0; Lab051 git add/commit и push не выполнялись. STOP. - **RF / Git:** RF TX=0; Lab051 git add/commit и push не выполнялись. STOP.
## Lab052
- **Цель:** снизить число ложных строгих проверок на 500 ксимв/с при сохранении
BPSK, PRBS7, PHY/FEC/CRC, фрагментации и всех принимаемых настоящих пакетов.
- **База:** Lab051 закреплена разрешённым коммитом
`9f33607d3ae82992c595de37dd64e99d7e4dea73`; 13 файлов, без push.
- **Профиль:** 1675842 локальных пика, 392785 кандидатов первого каскада;
прежний RX выполнил 147005 строгих проверок: 146765 ложных и 240 истинных.
Исторические 147082 Lab051 означали все кандидаты, а не только ложные.
- **Алгоритм:** общий дешёвый расчёт корреляций двух половин преамбулы,
локальная нормировка каждой половины; ограниченная полная ширина группы
20480 отсчётов, максимум первым, альтернативы сохраняются после FAIL.
Первый порог прежний; VALID только после штатного PHY/FEC/CRC.
- **Отвергнутый вариант:** безусловное подавление соседних меньших пиков
теряло слабый пакет рядом с сильной ложной преамбулой. Контрпример adversary
исправлен и закреплён тестами, включая границы блоков и плохую CRC.
- **Кандидаты:** 388407 отклонены дешёво, 384 группы, 243 представителя +12
альтернативных строгих проверок; 15 ложных, 240 настоящих, 0 потерь.
Сокращение ложных строгих проверок 9784,33×. Все 240 уникальных стартов совпали.
- **Полный поток:** C/5 JPEG в секунду, 30 изображений, 240/240 пакетов,
30/30 JPEG SHA MATCH. Медианный полный проход 7,369795 с /6,044416 с IQ
=1,219273. Возраст 234,520/834,077/1549,595 мс, все 30 просрочены,
**FRESH DISPLAY FPS=0; REAL-TIME CAPABLE=NO**.
- **Остаточная нагрузка:** настоящие пакеты 3,996005 с, общая децимация/
корреляция 1,790240 с. Все ложные проверки вместе лишь 28,884 мс.
- **Память:** удерживаемый IQ 3,031 МиБ, инженерная оценка массивов 136,161 МиБ,
измеренный process RSS peak 160,984 МиБ. Шумовой тест 16,78 с не накапливает
IQ/кандидатов; внешний live-backlog остаётся нереализованным.
- **Регрессии:** 37/45 слабых сценариев у обоих приёмников, 0 дополнительных
потерь. Старая запись 20 ксимв/с: 36/36, CRC PASS, JPEG SHA MATCH.
- **Код:** [обнаружитель](../experiments/lab052_candidate_detection.py),
[опыты](../experiments/lab052_detector_study.py),
[статистика](../experiments/lab052_detection_statistics.py),
[30 тестов](../tests/test_lab052_candidate_detection.py).
- **Отчёт:** [состояние](lab052_current_state.md),
[основной поток](../data/processed/lab052/stream.json). Журнал: запись 041.
- **Далее:** отдельное исследование истинных пакетов, общей обработки IQ и
задержки JPEG-пачки; текущий этап завершить на программных результатах.
- **RF / Git:** RF TX=0; Lab052 git add/commit и push не выполнялись. STOP.
## Правило хранения данных ## Правило хранения данных
Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные

View File

@@ -0,0 +1,390 @@
# Lab052 — снижение числа ложных кандидатов при 500 ксимв/с
Дата: 8 сентября 2026 года. SOFTWARE ONLY. RF TX = 0.
## Результат
Ложные строгие проверки сокращены с **146765 до 15**, в **9784,33 раза**
(99,9898%). Сохранены все **240 уникальных физических пакетов**, получены
**30/30 JPEG с точными байтами**. Программное исследование завершено.
**Реальное время и 5 свежих изображений/с не достигнуты.** Медианный из трёх
полных проходов: **7,369795 с обработки / 6,044416 с IQ = 1,219273**.
Возраст декодированных изображений: **234,520 / 834,077 / 1549,595 мс**
(min / median / max). При MAX_VIDEO_AGE=200 мс все 30 просрочены,
**DISPLAYED=FRESH DISPLAYED=0, FRESH DISPLAY FPS=0**.
В этих условиях ложные проверки уже не определяют скорость: они занимают
28,884 мс за весь опыт, 0,392% полной обработки. Дальнейшее удаление только
этих проверок не закроет оставшееся отставание.
Рабочая точка: BPSK, 500000 символов/с, Fs=8000000 отсчётов/с, 16 отсчётов/символ.
JPEG C: 128×96 RGB, quality 25, 1740–1893 байта. PRBS7, PHY, FEC, CRC,
интерливер, формат пакета и фрагмента не менялись. Файлы protocol не изменены.
Управление RadioMaster → ExpressLRS → штатный ELRS RX → CRSF → контроллер
остаётся отдельным физическим каналом; в SDR-поток управление не добавлялось.
Временные подмены старых функций рассчитаны на один последовательный RX-worker;
потокобезопасность и одновременная работа нескольких таких приёмников не реализованы.
## Git и аудит
Проверены root/main/HEAD/status, diff stat/check, отчёт Lab051, запись 040,
индекс лабораторных, три новых вычислительных модуля, тесты и итоговые JSON.
До новой реализации quick_gate PASS; pytest **688 passed, 0 failed**.
Verifier независимо подтвердил состав и согласованность данных.
По прямому разрешению пункта 0 задания показаны staged stat/status и создан:
`9f33607d3ae82992c595de37dd64e99d7e4dea73`
`Lab051: accelerate 500 ksymbol/s streaming receiver`
Ровно 13 файлов, 9695 добавлений: код/тесты/отчёт Lab051, журнал и индекс,
baseline_profile.json, benchmark.json, stream.json, memory.json, replay.json,
regression20k/result.json. IQ-окна, промежуточные замеры, JPEG и посторонние
файлы не добавлялись. Push не выполнялся. Это committed HEAD всей Lab052.
## Что означают исходные 147 тысяч
В исходном JSON Lab051 **147082 — все строгие кандидаты**, включая 240 принятых;
ложных было **146842**. Число 12239 — новые soft/timing-окна, включая настоящие.
Исторический processing/RF=6,391045, fresh FPS=0. Называть 147082 ложными неверно.
Lab051 не сохраняла измеренные encode_seconds каждого JPEG. Её длинная временная
сетка немного менялась между запусками. Поэтому в Lab052 тот же генератор,
noise seed=50050, coded-bit Eb/N0=7 дБ, CFO=+10 Гц, фаза 0,73, чередование
полярности и CS8-квантование используют **однократно измеренную и сохранённую
готовность JPEG** в source_fixture.json. Проверяются SHA всех 30 JPEG.
Все новые варианты получают одну и ту же последовательность IQ; сравнение
потерь и кандидатов ниже относится к этой общей сетке. Историческая сетка
Lab051 не объявляется побитно воспроизведённой.
Свежий профиль прежнего Lab051, после независимых решений:
| Метрика | Значение |
|---|---:|
| IQ / Fs | 6,044416 с / 8 Мвыб/с |
| TOTAL LOCAL PEAKS | 1675842 |
| FIRST-STAGE CANDIDATES | 392785 |
| STRICT CANDIDATES | 147005 |
| TRUE CANDIDATES | 240 |
| FALSE CANDIDATES | 146765 |
| CANDIDATES / RF SECOND | 24320,795 |
| FALSE CANDIDATES / RF SECOND | 24281,089 |
| Уникальных уточнённых позиций ложных кандидатов | 11387 |
| Повторных уточнённых позиций | 135378 |
Отдельный свежий проход старого Lab051 **без наблюдателя**, на этой же
зафиксированной сетке: **41,633312 с, processing/RF=6,887897**, 240/240 пакетов
и 30/30 точных JPEG. Сравнение с итоговыми 7,369795 с даёт около **5,65×**
ускорения всего приёмника. Относительно исторических 38,630132 с — **5,24×**.
Инструментированный профиль и чистый контроль хранятся раздельно; настольная
нагрузка и прогрев влияют на временные числа.
Сначала детектор получает каждый локальный максимум выше низкого фиксированного
порога. Только затем пропускает область уже принятого пакета. На продолжительном
шуме число проб растёт с числом входных отсчётов. Нормировка на энергию означает,
что малая абсолютная мощность шума сама по себе не уменьшает корреляционную
статистику. Более 135 тысяч проверок вновь приходят к уже встречавшимся позициям.
PRBS7 повторяется через 127 символов, то есть через 2032 отсчёта в рабочем режиме.
Преамбула содержит 1280 символов. Поэтому рядом с ней возможны сильные боковые
корреляционные пики, помимо случайных шумовых максимумов. Только проверка
структуры и последующий PHY/FEC/CRC различают их надёжнее одного большого пика.
## Распределения и фон
Истинные главные пики определены после CRC PASS: берётся максимум в прежней
области уточнения около восстановленного старта. Это диагностическая метрика,
не дополнительный вход алгоритма. Числа истинных кадров также сверены по
множеству всех 240 уникальных уточнённых стартов, а не только по счётчику.
| Корреляционная статистика | min | median | p99 | max |
|---|---:|---:|---:|---:|
| Главный пик настоящей преамбулы, 240 | 0,830055 | 0,841691 | 0,848389 | 0,849945 |
| Ложные строгие кандидаты, 146765 | 0,030964 | 0,035464 | 0,055803 | 0,805951 |
| Меньшая корреляция половин у принятых кандидатов | 0,825038 | 0,837340 | 0,845914 | 0,846226 |
Отдельно после приёма выделены 357 блоков, чьи корреляционные окна не пересекают
ни одного декодированного физического кадра. Медиана блочных медиан =0,016353,
медиана MAD =0,006184, медиана квантилей 99% =0,041875.
Диапазон медиан 0,006451–0,017422 показывает, что фон не идеально стационарен.
MAD — медиана абсолютного отклонения от медианы; здесь это мера разброса.
Полные min/median/MAD/p90/p99/p999/max по блокам сохранены в JSON.
Адаптацию порога по median/MAD рассмотрели, но не выбрали: она не устраняет
повторные структурные пики и потребовала бы отдельно доказывать чувствительность
при смене фона. Основное правило использует **локальную энергию каждой половины**
наблюдаемого IQ. Глобальный median/second или TX reference в решении не участвуют.
На меняющемся фоне и в слабых положительных регрессиях дополнительные потери
не наблюдались; универсальная чувствительность при любом нестационарном канале
этим конечным исследованием не доказана.
![Распределения обнаружителя](../data/processed/lab052/detector_distributions.png)
## Выбранное правило
1. Прежняя грубая PRBS7-корреляция на двух отсчётах/символ и прежний первый
порог **0.030964473262429237**. Все локальные пики считаются одинаково с baseline.
2. На уже децимированном общем входе блока считаются корреляции двух половин
опорной формы. Полная форма содержит 2644 отсчёта, каждая половина — 1322.
Для каждой позиции обе величины нормируются **своими** энергиями IQ и опоры.
Требуются обе корреляции ≥**0.08474038063392896**. Одиночная сильная половина
этого условия не выполняет. Неизвестная фаза/полярность сохраняется через
модуль; согласование фаз половин, сужающее CFO, не вводилось.
3. Прошедшие пики объединяются в группы с полной шириной ≤20480 отсчётов
**от первого пика**, без продления по цепочке следующих соседей. На границе
блока группа обрывается: это может добавить работу, но не скрывает следующий пик.
4. Представитель — максимум нормированной корреляции; при равенстве правый пик.
Если он не проходит строгий тракт, проверяется следующая альтернатива группы.
Только реальный CRC PASS разрешает прежний пропуск принятого физического кадра.
5. VALID остаётся исключительно прежним **PHY → FEC → CRC PASS**.
Корреляции частей — общие FFT на блок, не новое timing-окно для каждого пика.
Это дополнительные согласованные фильтры частей преамбулы на децимированном
сигнале. Повторно использовать полный RRC-выход для предварительного решения
не потребовалось: его создание на шумовых блоках дороже этой проверки.
Полный RRC/timing и все 1601 прежних смещения остались в строгом тракте.
Порог половины выбран до положительных сравнений из номинальной шумовой модели
`|rho|² ~ Beta(1,N−1)`, N=640, alpha=0,01 на часть:
`sqrt(1−alpha**(1/(N−1)))`. Это инженерная исходная настройка, **не** доказанная
вероятность ложного принятия после фильтрации и выбора максимумов. Корреляция
отсчётов и отбор пиков нарушают независимость; фактические отказы измерены отдельно.
Ни порог, ни ширина не выбираются по TX offsets.
Обоснование ширины для двух разных последовательных физических кадров:
`MIN_FRAME_SAMPLES=(1280+144+2*(12*8+6)+32+10)*16=26720`.
Точность прежнего локального уточнения ±2000 плюс сетка децимации 8:
`20480 < 26720−2*2008 = 22704`.
Поэтому два центральных кандидата неперекрывающихся минимальных кадров,
локализованных в этом диапазоне, не попадают в одну группу. Это обоснование
для последовательного потока, не обещание разрешить любые радиоколлизии.
## Почему первый вариант был отвергнут
Изначальное подавление всех меньших пиков в радиусе одной преамбулы дало
0 ложных строгих проверок на основном опыте. Adversary воспроизвёл потерю:
ложная сильная преамбула на отсчёте 10000 без правильного заголовка подавляла
настоящий слабый пакет на 30000. Lab051 его принимала, новый вариант — нет.
Одного обоснования минимальной дистанцией двух **настоящих** кадров недостаточно
для случая сильного ложного пика. Этот вариант **отвергнут**.
Исправление: дешёвая проверка всех локальных кандидатов до выбора представителя,
ограниченная полная ширина группы и сохранение альтернатив после FAIL.
Контрпример, его сдвиги через границу IQ-блока и сильный пакет с плохой CRC
перепроверены независимо. Корректный слабый пакет теперь сохраняется.
Итог Lab052 — **15**, а не предварительные 0 ложных проверок.
## Сравнение числа частей
Во всех четырёх вариантах сохранены те же 240 уникальных стартов и 30 точных JPEG.
Наблюдатель собирал полные распределения; время этих проходов не является
основным измерением производительности.
| Правило | Порог части | Ложные строгие проверки | Сокращение | Потеря настоящих |
|---|---:|---:|---:|---:|
| Только группы, с альтернативами после FAIL | — | 145863 | 1,006× | 0 |
| Одна часть, контроль скалярного порога | 0,059951 | 332 | 442,06× | 0 |
| **Две части, основной вариант** | **0,084740** | **15** | **9784,33×** | **0** |
| Три части | 0,103813 | 6 | 24460,83× | 0 |
Одна часть — только сравнительный контроль, не принятый ответ «поднять порог».
Третья часть убирает ещё девять проверок, но увеличивает стоимость общего
этапа: 0,531 → 0,819 с в инструментированных проходах. Две части — минимальный
вариант с проверкой распределённой структуры и большим запасом к ориентиру 50×.
Группировка сама по себе с корректным возвратом альтернатив почти не помогает
на распределённом шуме: отказы нельзя использовать для безусловного удаления соседей.
## Окончательные вычисления и свежесть
Три отдельных полных прохода без наблюдателя: processing/RF
**1,257688 / 1,219199 / 1,219273**, в каждом 240/240 пакетов, 30/30 JPEG,
15 ложных проверок и fresh FPS=0. Выбран целиком третий проход — медианный
по суммарному времени, без смешивания временных отметок разных запусков.
| Стадия / счётчик | Итог |
|---|---:|
| TOTAL LOCAL PEAKS | 1675842 |
| FIRST-STAGE / CHEAP CHECKED | 392785 |
| CHEAP REJECTED | 388407 |
| CHEAP RETAINED | 4378 |
| CLUSTERS, включая группы внутри уже принятых кадров | 384 |
| REPRESENTATIVE / ALTERNATIVE STRICT CHECKS | 243 / 12 |
| Пропущено внутри принятого физического кадра | 4123 |
| STRICT FALSE / TRUE | 15 / 240 |
| STRICT CHECKS / RF SECOND | 42,187699 |
| FALSE CHECKS / RF SECOND | 2,481629 |
| Новые soft/timing-окна | 488 |
| Грубая корреляция и децимация | 1,790240 с |
| Общие проверки частей | 0,446616 с |
| Группировка | 0,003113 с |
| Строгие настоящие / ложные | 3,996005 / 0,028884 с |
| Среднее настоящего кандидата | 16,650 мс |
| Суммарно RX / сборка / JPEG decode | 7,358399 / 0,006008 / 0,005388 с |
| Полная обработка | 7,369795 с |
| PROCESSING / RF | 1,219273 |
| INPUT FPS / FRAMES | 5 / 30 |
| RX PACKETS / IMAGES | 240 / 30 |
| DROPPED BEFORE TX / STALE AFTER DECODE | 0 / 30 |
| DISPLAYED / FRESH DISPLAYED / FRESH FPS | 0 / 0 / 0 |
| RAW RECEIVED FPS в первые 6 с шкалы работника | 4 |
| Возраст декодированных JPEG min / median / max | 234,520 / 834,077 / 1549,595 мс |
| MAX_VIDEO_AGE | 200 мс |
После устранения основной массы ложных кандидатов обнаружена лишняя подготовка:
старый адаптер очищал кэш и 734 раза строил ту же опорную форму. В начальном
cProfile это занимало 3,544 с. В Lab052 три неизменных массива — опора, RRC taps
и PRBS7 bits — хранятся в самом приёмнике. Они побитно сравнены со старым
расчётом; функции возвращаются в finally до выхода старого контекста режима.
Это не новая PHY и не передача данных TX в RX.
Финальный отдельный cProfile только вокруг push/finish показывает следующие
узкие места: FFT 1,650 с собственного времени, FIR-децимация 1,295 с,
Viterbi 1,397 с; уточнение 1,084 с и matched 0,865 с включают свои FFT,
поэтому эти строки нельзя складывать повторно. Копирование ndarray — 0,313 с,
прочие расходы push — также измерены. Данные cProfile не заменяют чистый замер.
Теперь основное ограничение — **настоящие пакеты вместе с общей обработкой IQ**.
40 пакетов/с ×16,650 мс требуют около 666 мс обработки в секунду; к ним добавляются
децимация/корреляция, общий промежуточный этап, копирование и организация блоков.
Во время JPEG-пачки пакет приходит каждые 11,532 мс, быстрее средней строгой
обработки настоящего пакета. Нужны и дальнейшее снижение общей стоимости,
и проверка задержки пачки. Одного среднего processing/RF<1 недостаточно для
доказательства MAX_VIDEO_AGE=200 мс при неравномерной нагрузке.
Событийная шкала повторно использует Lab051:
`worker_start=max(previous_worker_end, block_arrival); worker_end=start+measured_push`.
Сборка/JPEG decode добавляют измеренные затраты. RF и CPU перекрываются;
стоимость RX не заменяется нулём. Генерация IQ, сравнение SHA и JSON I/O исключены.
Восстановленные байты сверяются с TX только после принятия, сборки и JPEG decode.
Возраст относится к **декодированным, затем отброшенным** изображениям.
Камера и физический экран не измерялись, их задержки UNKNOWN. Capture lookup
используется после вердикта для отчётной модели вывода. В неизменном формате
фрагмента нет capture timestamp; независимый live-контроль возраста не реализован.
Максимальное отставание 1,426779 с соответствует примерно 91313848 байтам
внешнего complex64 IQ. Этот backlog вычислен, но не хранится офлайн-источником.
REAL-TIME CAPABLE=NO; 5 FPS SOFTWARE REAL-TIME TARGET=FAIL.
## Память и длительное состояние
- Пик удерживаемого IQ: **3177728 байт = 3,031 МиБ**; проверяемый предел 3398912.
- Инженерная оценка рабочих массивов: **142775456 байт = 136,161 МиБ**.
Это прежний запас Lab051, включающий буферы; не доказанный универсальный
предел C-аллокаций и не отдельная величина для сложения с RSS.
- Оценка массивов именно обнаружителя в опыте: **1480664 байта**;
постоянные новые шаблоны — **171140 байт**. Временные FFT отдельно целиком
не перечислены, они покрываются общей инженерной оценкой с запасом.
- RSS выбранного полного прохода: **149393408 байт** после потока;
process RSS peak **168804352 байта = 160,984 МиБ**. Это весь процесс Python,
включая библиотеки, генератор и аллокатор, а не только массивы RX.
- В основном опыте одновременно максимум **23** прошедших дешёвый этап пика
и **3** группы на блок; между вызовами и после finish состояние кандидатов =0.
- Предел числа кандидатов задаётся числом локальных максимумов ограниченного
IQ-окна. Списки групп и альтернатив уничтожаются на выходе, не копятся в сессии.
Дополнительно измерен отдельный RX replay длительностью 0,063184 с IQ с одним
независимо восстановленным пакетом. Наблюдаются 6754 ndarray, из них 6681 владеют
данными; суммарные байты наблюдаемых владельцев за проход — 37378942 (не пик).
**tracemalloc peak=11586792 байта**, после выхода остаётся 6461 байт отслеживаемых
выделений. Входной IQ подготовлен до tracing. Это измерение отслеживаемой памяти
RX, включая Python, и нижняя граница числа массивов в локальных переменных;
временные C/SciPy-аллокации полностью не перечислены. Короткий replay не заменяет
RSS полного опыта. Подсчёт ndarray и tracemalloc выполняются в разных проходах;
tracemalloc измеряет следующий прогретый проход. Его tracing-время не используется
для оценки скорости, а пик не объявляется верхней границей холодного RX.
Длинный тест: **2048 блоков, 16,777216 с** распределённого шума с амплитудными
масштабами 0,002 / 0,2 / 20 / 0,02. Получены 4783206 локальных пиков,
1068038 кандидатов первого каскада; 1068037 отклонены дешёво, один дошёл
до строгого отказа. **Ни одного принятого шумового пакета**.
После прогрева в контрольных точках IQ=2653440 байт, децимация=397216 байт,
сохранённых кандидатов=0. RSS сначала растёт при прогреве кэшей, затем
стабилизируется; последние четыре точки 140533760–140550144 байта.
После finish оба IQ-буфера освобождены, константы ограниченного размера остаются.
Полная диагностическая история NPZ/JSON принадлежит конечному эксперименту,
не приёмнику. Она выключена в окончательном измерении скорости и длинном тесте.
Ограниченный RX не подменяет отсутствующую живую входную очередь с overflow/gap
policy: аппаратный драйвер и переполнения в этой лабораторной не реализованы.
## Регрессии и проверка
45 дополнительных слабых сценариев: Eb/N0=0/1/2/3/7 дБ, CFO=−100/+10/+100 Гц,
три seed, масштабы амплитуды 0,002/0,2/1 и разные фазы/полярности.
Старый и новый RX правильно приняли **37/45**; **0** потерянных пакетов baseline.
Восемь случаев не принимаются обоими, поэтому это не объявляется 45/45 успешным
декодированием. Основной поток отдельно даёт 240/240 и 30/30 SHA MATCH.
30 быстрых целевых тестов покрывают шумовые группы, равные пики, отсутствие
цепочного расширения, два минимальных соседних кадра, слабый кадр, одну сильную
ложную половину, фазу/масштаб, распределённый и меняющийся шум, границы блоков,
освобождение состояния, эквивалентность общих корреляций отдельным vdot,
ложную преамбулу и восстановление функций. Verifier и adversary независимо
подтвердили исправление воспроизведённой потери; обхода PHY/FEC/CRC не найдено.
Сохранённая Lab047-запись **20 ксимв/с** повторно разобрана прежним Lab048:
**36/36, CRC PASS, JPEG SHA MATCH**, 19 ошибок до FEC, 0 после, 0 байтовых ошибок.
SHA: `e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a`.
Обработка 15 с старого IQ заняла 13,213346 с. Новый адаптер принимает только
500 ксимв/с; режим 20 ксимв/с, его порог и вычислительный тракт не менялись.
Итоговый quick_gate PASS: **718 тестов, 20 функциональных проверок,
13 штатных пропусков**. Полный pytest: **718 passed за 77,15 с, 0 failed**;
одно предупреждение доступа к .pytest_cache не влияет на результат тестов.
Дополнительный helper измерения массивов выполнен отдельно, все три модуля
после его добавления проверены py_compile и своими run_functional_tests.
git diff --check PASS. Машинная сверка итоговых JSON — в acceptance.json:
обязательные регрессии PASS, real-time и 5-fps target явно FAIL.
## Файлы и воспроизведение
- [Обнаружитель и потоковый адаптер](../experiments/lab052_candidate_detection.py)
- [Генератор сравнений, измерений и регрессий](../experiments/lab052_detector_study.py)
- [Статистика и график](../experiments/lab052_detection_statistics.py)
- [Целевые тесты](../tests/test_lab052_candidate_detection.py)
- [Профиль старого RX](../data/processed/lab052/baseline_profile.json)
- [Старый RX без наблюдателя на том же источнике](../data/processed/lab052/baseline_stream.json)
- [Сравнение правил](../data/processed/lab052/rule_comparison.json)
- [Статистика после решений](../data/processed/lab052/statistics.json)
- [Итоговый полный поток](../data/processed/lab052/stream.json)
- [Длинное состояние](../data/processed/lab052/memory.json)
- [Измеренные массивы и tracemalloc](../data/processed/lab052/allocations.json)
- [Слабые сигналы](../data/processed/lab052/sensitivity.json)
- [Отдельный профиль CPU](../data/processed/lab052/receiver_cpu.txt)
- [Старая запись 20 ксимв/с](../data/processed/lab052/regression20k/result.json)
- [Сверка критериев и отрицательного результата скорости](../data/processed/lab052/acceptance.json)
```powershell
python experiments/lab052_detector_study.py --stage profile
python experiments/lab052_detector_study.py --stage baseline
python experiments/lab052_detector_study.py --stage rules
python experiments/lab052_detector_study.py --stage stream
python experiments/lab052_detector_study.py --stage cpu
python experiments/lab052_detector_study.py --stage memory
python experiments/lab052_detector_study.py --stage sensitivity
python experiments/lab052_detection_statistics.py
python -m pytest tests/test_lab052_candidate_detection.py -q -p no:cacheprovider
python tools/quick_gate.py
python -m pytest tests/ -q
git diff --check
```
Старый IQ: команда Lab048 с параметрами `--run-saved-capture`,
`--capture C:\Users\user\Documents\SDR_Rover_Lab047_raw\20260903_115747\rx_stream.cs8`,
`--original-result C:\Users\user\Documents\SDR_Rover_Lab047_raw\20260903_115747\independent_result.json`,
`--tx-image data/processed/lab047/tx_image.jpg`,
`--output-directory data/processed/lab052/regression20k`.
Первый запуск фиксирует измеренную готовность JPEG; последующие используют
source_fixture.json. Между запусками меняются фактические CPU timings и RSS,
поэтому временные числа могут отличаться. Основной приём и набор кандидатов
на зафиксированном источнике воспроизводятся. Большой новый IQ-файл не создаётся.
Lab052 git add/commit и push не выполнялись. RF TX=0; HackRF и другое
оборудование не перечислялись и не запускались. QPSK, 180 МГц, аппаратные
500 ксимв/с и переход к 10 fps не выполнялись. Следующий рекомендуемый этап —
отдельная оптимизация истинных пакетов/общего IQ и задержки JPEG-пачки. STOP.

View File

@@ -0,0 +1,287 @@
"""Lab052: локальные группы пиков и дешёвая проверка частей PRBS7.
Кандидат никогда не становится пакетом без прежних PHY/FEC/CRC. Параметры
заданы длинами PHY и моделью шума, без ожидаемых стартов и содержимого TX.
"""
from __future__ import annotations
from collections import Counter
from contextlib import contextmanager
from dataclasses import dataclass
import math
from pathlib import Path
import sys
import time
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import numpy as np
from scipy.signal import fftconvolve
from experiments import lab051_streaming_receiver as previous
phy = previous.phy
offline = previous.offline
SAMPLES_PER_SYMBOL = 16
PREAMBLE_SAMPLES = phy.lab045.PREAMBLE_BIT_COUNT * SAMPLES_PER_SYMBOL
COARSE_ERROR_SAMPLES = 2000 + 8 # исходное уточнение ±2000 плюс сетка децимации
MIN_FRAME_SAMPLES = (phy.lab045.PREAMBLE_BIT_COUNT + phy.lab045.PHY_HEADER_BIT_COUNT
+ phy.lab045.conv.encoded_bit_count(phy.lab045.MIN_PACKET_BYTES * 8)
+ 2 * phy.lab045.GUARD_SYMBOL_COUNT + phy.lab045.RRC_SPAN_SYMBOLS) * SAMPLES_PER_SYMBOL
CLUSTER_WIDTH_SAMPLES = PREAMBLE_SAMPLES
PART_FALSE_ALARM = .01
@dataclass(frozen=True)
class DetectorRule:
"""parts=0 оставляет только группировку; 1/2/3 нужны для сравнения правил."""
parts: int = 2
cluster_width_samples: int = CLUSTER_WIDTH_SAMPLES
def __post_init__(self):
if type(self.parts) is not int or not 0 <= self.parts <= 3:
raise ValueError("Нужно от нуля до трёх частей")
if (type(self.cluster_width_samples) is not int or self.cluster_width_samples < 0
or self.cluster_width_samples % 8
or self.cluster_width_samples >= MIN_FRAME_SAMPLES - 2 * COARSE_ERROR_SAMPLES):
raise ValueError("Окно должно различать два минимальных физических кадра")
def peak_groups(indexes: np.ndarray, width: int):
"""Полная ширина от первого пика, без продолжения по цепочке соседей.
На границе IQ-блока группа обрывается: возможна лишняя проверка, но ни
один следующий пик не теряется из-за представителя из другого блока.
"""
cursor = 0
while cursor < len(indexes):
end = int(np.searchsorted(indexes, indexes[cursor] + width, side="right"))
yield indexes[cursor:end]
cursor = end
def cluster_peak_indexes(scores: np.ndarray, width: int, threshold: float) -> np.ndarray:
"""Первые представители ограниченных групп; равенство → правый пик."""
peaks = offline._all_local_peak_indexes(scores)
peaks = peaks[scores[peaks] >= threshold]
return np.asarray([group[len(group) - 1 - int(np.argmax(scores[group[::-1]]))]
for group in peak_groups(peaks, width)], dtype=np.int64)
def part_correlations(values: np.ndarray, reference: np.ndarray, count: int) -> np.ndarray:
"""Несколько vdot на уже децимированном согласованном входе, без timing.
Модуль сохраняет неизвестную фазу/полярность и не вводит ограничения CFO
через согласование фаз между половинами. Каждая часть нормируется своей
энергией, поэтому усиление и смена мощности фона не требуют TX reference.
"""
if values.size != reference.size or count < 1:
return np.zeros(max(count, 0))
boundaries = np.linspace(0, reference.size, count + 1, dtype=int)
output = np.zeros(count)
for number, (low, high) in enumerate(zip(boundaries[:-1], boundaries[1:])):
part = values[low:high]
expected = reference[low:high]
energy = float(np.vdot(part, part).real) * float(np.vdot(expected, expected).real)
output[number] = abs(np.vdot(expected, part)) / math.sqrt(max(energy, 1e-30))
return output
def part_threshold(count: int) -> float:
"""Номинальная H0-модель: независимые комплексные отсчёты по символам.
|rho|²~Beta(1,N-1), alpha=0.01 на часть. Это исходная инженерная настройка,
не обещание точной вероятности после RRC/выбора пиков: фон проверяется отдельно.
Она фиксируется до положительных опытов и не подгоняется под их offsets.
"""
symbols = phy.lab045.PREAMBLE_BIT_COUNT // count
return math.sqrt(1 - PART_FALSE_ALARM ** (1 / (symbols - 1)))
class CandidateFrontEnd(previous.BlockFrontEnd):
"""Общая корреляция — согласованный фильтр преамбулы на двух выборках/символ.
Промежуточному этапу достаточно его исходных данных; полный общий RRC-
выход создаётся прежним строгим трактом только при прошедшем кандидате.
"""
def coarse_scores(self, samples, reference):
raw = self.decimator.get(samples, self.base)
mean = np.mean(raw, dtype=np.complex128).astype(np.complex64)
needed = math.ceil(self.owned_end / 8) + reference.size + 1
self.detection_values = raw[:needed].copy()
self.detection_values -= mean
self.detection_values *= previous.detection_rotation(self.detection_values.size, 8)
if self.detection_values.size < reference.size:
return np.empty(0, np.float32)
scores = previous.lab043._normalized_sliding_correlation(self.detection_values, reference).astype(np.float32)
scores[~np.isfinite(scores)] = 0
np.clip(scores, 0, 1, out=scores)
return scores
def split_scores(self, reference, count, score_count):
"""Общие корреляции частей для всех кандидатов, без per-candidate FFT.
Проверка выполняется ДО выбора представителя: сильный случайный пик
одной половины не может скрыть согласованный слабый кандидат рядом.
"""
output = np.empty((score_count, count), np.float32)
boundaries = np.linspace(0, reference.size, count + 1, dtype=int)
energy = np.concatenate(([0.], np.cumsum(np.abs(self.detection_values) ** 2, dtype=np.float64)))
for number, (low, high) in enumerate(zip(boundaries[:-1], boundaries[1:])):
expected = reference[low:high]
correlation = fftconvolve(self.detection_values[low:], np.conj(expected[::-1]), mode="valid")[:score_count]
local_energy = energy[high:high + score_count] - energy[low:low + score_count]
denominator = np.sqrt(np.maximum(local_energy * float(np.vdot(expected, expected).real), 1e-30))
output[:, number] = np.abs(correlation) / denominator
return output
class StreamingReceiver(previous.StreamingReceiver):
"""Ограниченный IQ и кандидаты только одного блока, API получает только IQ."""
def __init__(self, symbol_rate: int = 500000, *, block_samples: int = 65536,
rule: DetectorRule = DetectorRule(), observer=None):
super().__init__(symbol_rate, block_samples=block_samples)
self.rule, self.observer = rule, observer
self.detection_statistics = Counter()
self.cheap_seconds = self.cluster_seconds = 0.0
self.true_strict_seconds = self.false_strict_seconds = 0.0
self.peak_cluster_count = self.peak_eligible_count = self.candidate_state_count = 0
self.working_detection_arrays_peak_bytes = 0
with previous.previous.streaming_mode(self.mode):
self.raw_reference = phy.lab045.lab044._preamble_reference_samples().copy()
self.matched_taps = previous.lab043._matched_filter_taps().copy()
self.preamble_bits = phy.lab045.lab044.preamble_bits().copy()
for values in (self.raw_reference, self.matched_taps, self.preamble_bits):
values.flags.writeable = False
@contextmanager
def constant_templates(self):
"""Старый адаптер очищает lru на каждом push; эти три константы локальны.
Возвращаем именно функции до выхода streaming_mode, чтобы его
cache_clear и соседний старый режим работали по прежним правилам.
"""
targets = ((phy.lab045.lab044, "_preamble_reference_samples", lambda: self.raw_reference),
(previous.lab043, "_matched_filter_taps", lambda: self.matched_taps),
(phy.lab045.lab044, "preamble_bits", lambda: self.preamble_bits))
originals = [(owner, name, getattr(owner, name)) for owner, name, function in targets]
try:
for owner, name, function in targets:
setattr(owner, name, function)
yield
finally:
for owner, name, function in reversed(originals):
setattr(owner, name, function)
def _scan(self, owned_end):
with self.constant_templates():
return self._scan_with_templates(owned_end)
def _scan_with_templates(self, owned_end):
width = self.rule.cluster_width_samples // self.decimation
front = CandidateFrontEnd(self.buffer, owned_end - self.base_sample,
self.front_statistics, decimator=self.decimator, base=self.base_sample)
started = time.perf_counter()
scores = front.coarse_scores(self.buffer, self.reference)
self.coarse_seconds += time.perf_counter() - started
threshold = np.float32(phy.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD)
peaks = offline._all_local_peak_indexes(scores)
offsets = self.base_sample + peaks * self.decimation
owned = (offsets >= self.scan_sample) & (offsets < owned_end)
owned_peaks = peaks[owned]
first_stage = owned_peaks[scores[owned_peaks] >= threshold]
self.detection_statistics["total_local_peaks"] += len(owned_peaks)
self.detection_statistics["first_stage_candidates"] += len(first_stage)
self.peak_candidate_count = max(self.peak_candidate_count, len(first_stage))
if self.observer is not None:
self.observer.scores(scores, self.base_sample, self.scan_sample, owned_end, self.decimation)
started = time.perf_counter()
parts = (front.split_scores(self.reference, self.rule.parts, len(scores))
if self.rule.parts and len(scores) else np.empty((len(scores), 0)))
passed = (np.all(parts[first_stage] >= part_threshold(self.rule.parts), axis=1)
if self.rule.parts else np.ones(len(first_stage), dtype=bool))
self.cheap_seconds += time.perf_counter() - started
self.detection_statistics["cheap_stage_checked"] += len(first_stage)
self.detection_statistics["cheap_stage_rejected"] += int(np.sum(~passed))
indexes = first_stage[passed]
if self.observer is not None and hasattr(self.observer, "cheap"):
for index, keep in zip(first_stage, passed):
self.observer.cheap(self.base_sample + int(index) * self.decimation,
float(scores[index]), parts[index], bool(keep))
started = time.perf_counter()
groups = list(peak_groups(indexes, width))
self.cluster_seconds += time.perf_counter() - started
self.detection_statistics["clusters"] += len(groups)
self.peak_cluster_count = max(self.peak_cluster_count, len(groups))
self.candidate_state_count = len(indexes)
self.peak_eligible_count = max(self.peak_eligible_count, len(indexes))
self.working_detection_arrays_peak_bytes = max(self.working_detection_arrays_peak_bytes,
front.detection_values.nbytes + scores.nbytes * 4 + parts.nbytes + (len(peaks) + len(indexes)) * 24)
emitted = []
try:
with previous.optimized_context(front):
# Внутри группы — самый сильный первым. Отказ не даёт права
# подавить альтернативы: пробуем следующий. Только CRC PASS
# разрешает прежний пропуск уже принятого физического кадра.
ordered = [group[np.argsort(scores[group], kind="stable")[::-1]] for group in groups]
attempted_groups = set()
for group_number, index in ((number, item) for number, group in enumerate(ordered) for item in group):
absolute = self.base_sample + int(index) * self.decimation
# Сетка грубого поиска может округлить следующий стык на
# одну выборку назад. Не терять соседний физический кадр.
if absolute < self.accepted_until - self.decimation:
self.detection_statistics["inside_accepted_frame"] += 1
continue
self.detection_statistics["alternative_checks" if group_number in attempted_groups
else "representative_checks"] += 1
attempted_groups.add(group_number)
self.candidate_count += 1
candidate = offline.CoarseCandidate(int(index) * self.decimation, float(scores[index]))
started = time.perf_counter()
outcome = offline.decode_candidate_from_memmap(
self.buffer.view(np.float32), self.buffer.size, candidate, self.candidate_count,
read_iq_window=lambda raw, low, high: raw[2 * low:2 * high].view(np.complex64))
elapsed = time.perf_counter() - started
self.strict_seconds += elapsed
if self.observer is not None:
self.observer.outcome(self.base_sample, candidate, outcome)
if outcome.accepted and outcome.crc == phy.lab045.CRC_PASS and outcome.frame is not None:
self.true_strict_seconds += elapsed
self.accepted_until = self.base_sample + outcome.next_sample
self.packet_count += 1
emitted.append(previous.previous.ReceivedPacket(
self.base_sample + outcome.refined_start_sample, self.accepted_until,
self.total_samples, bytes(outcome.frame.reception.payload_bytes)))
else:
self.false_strict_seconds += elapsed
self.rejected_reasons[outcome.reason] += 1
finally:
self.candidate_state_count = 0
self.blocks_processed += 1
self.scan_sample = owned_end
retain_start = max(0, self.scan_sample - self.margin)
trim = retain_start - self.base_sample
if trim > 0:
self.buffer = self.buffer[trim:].copy()
self.base_sample = retain_start
return emitted
def run_functional_tests() -> None:
assert CLUSTER_WIDTH_SAMPLES < MIN_FRAME_SAMPLES - 2 * COARSE_ERROR_SAMPLES
assert .08 < part_threshold(2) < .09
assert StreamingReceiver().finish() == []
def main() -> None:
run_functional_tests()
print("Lab052 detector functional checks PASS; RF TX=0")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,157 @@
"""Lab052: распределения и численные проверки после независимых решений RX."""
from __future__ import annotations
import json
from pathlib import Path
import sys
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import numpy as np
from experiments.lab052_detector_study import OUTPUT, quantiles, write_result
from experiments import lab052_candidate_detection as detection
from experiments.lab051_receiver_profile import array_observation
def measure_arrays() -> dict:
"""Отдельный короткий RX replay, без смешивания tracing с замером скорости.
ndarray в Python-locals даёт нижнюю границу наблюдаемых массивов; C/SciPy
могут иметь невидимые временные выделения. tracemalloc peak — отдельная
измеренная величина, не RSS и не универсальная верхняя граница памяти.
"""
physical = detection.phy.lab045
payload = bytes(range(256))
plan = physical.build_transmission_plan(payload)
raw = detection.phy.build_parameterized_tx_cs8(plan, detection.phy.PhyMode(500000))
components = np.frombuffer(raw, np.int8).astype(np.float32)
values = components[0::2] + 1j * components[1::2]
random_generator = np.random.default_rng(52040)
sigma = np.sqrt(float(np.sum(abs(values)**2)) / plan.frame_symbol_count / (2 * 10**.7))
values += sigma * (random_generator.normal(size=len(values)) + 1j*random_generator.normal(size=len(values)))
samples = .2 * (random_generator.normal(size=63217 + len(values) + 350000)
+ 1j * random_generator.normal(size=63217 + len(values) + 350000))
samples[63217:63217 + len(values)] = values
samples = samples.astype(np.complex64)
def replay():
receiver = detection.StreamingReceiver()
packets = []
for start in range(0, len(samples), receiver.block_samples):
packets.extend(receiver.push_iq_block(samples[start:start + receiver.block_samples]))
packets.extend(receiver.finish())
assert len(packets) == 1 and packets[0].payload == payload
result = {**array_observation(replay), "iq_seconds": len(samples) / 8000000,
"packets": 1, "interpretation": "observed Python ndarray lower bound; tracked RX allocation peak; input IQ prepared before tracing",
"timing_not_used_as_benchmark": True, "rf_tx": 0}
write_result("allocations.json", result)
return result
def analyze() -> dict:
baseline = json.loads((OUTPUT / "baseline_profile.json").read_text(encoding="utf-8"))
optimized = json.loads((OUTPUT / "parts2.json").read_text(encoding="utf-8"))
before = np.load(OUTPUT / "baseline_observations.npz")
after = np.load(OUTPUT / "parts2_observations.npz")
peaks, outcomes = before["peaks"], before["outcomes"]
true = outcomes[outcomes[:, 3] == 1]
false = outcomes[outcomes[:, 3] == 0]
starts = np.sort(true[:, 2].astype(np.int64))
new_starts = np.sort(after["outcomes"][after["outcomes"][:, 3] == 1, 2].astype(np.int64))
missing = np.setdiff1d(starts, new_starts)
unexpected = np.setdiff1d(new_starts, starts)
main_scores = []
for start in starts:
low, high = np.searchsorted(peaks[:, 0], [start - detection.COARSE_ERROR_SAMPLES,
start + detection.COARSE_ERROR_SAMPLES], side="left")
main_scores.append(float(peaks[low:high, 1].max()))
# Окна чистого фона выделяются ТОЛЬКО после независимых принятых кадров.
# В этом опыте decoded PHY packet 268B => waveform 92256 samples.
frame_samples = (detection.phy.lab045.PREAMBLE_BIT_COUNT + detection.phy.lab045.PHY_HEADER_BIT_COUNT
+ detection.phy.lab045.conv.encoded_bit_count((256 + detection.phy.lab045.MIN_PACKET_BYTES) * 8)
+ 42) * 16
blocks = baseline["detection_profile"]["blocks"]
noise_blocks = []
for block in blocks:
low, high = block["start_sample"], block["end_sample"] + (1280 + 42) * 16
if not np.any((starts < high) & (starts + frame_samples > low)):
noise_blocks.append(block)
noise_statistics = {name: quantiles([block["score_statistics"][name] for block in noise_blocks])
for name in ("median", "mad", "p90", "p99", "p999")}
cheap = np.load(OUTPUT / "parts2_cheap.npy")
accepted_coarse = after["outcomes"][after["outcomes"][:, 3] == 1, 0]
is_true = np.isin(cheap[:, 0], accepted_coarse)
part_min = cheap[:, 3:].min(axis=1)
false_count = len(false)
new_false = optimized["detector"]["strict_false_candidates"]
result = {
"classification": "strict PHY/FEC/CRC verdict first; no diagnostic feedback into RX",
"baseline_true_unique_starts": len(np.unique(starts)),
"optimized_true_unique_starts": len(np.unique(new_starts)),
"missing_true_starts": missing.tolist(), "unexpected_true_starts": unexpected.tolist(),
"true_main_peak_scores": quantiles(main_scores), "false_strict_scores": quantiles(false[:, 1]),
"baseline_false_refined_positions": len(np.unique(false[:, 2])),
"baseline_false_repeated_refinements": len(false) - len(np.unique(false[:, 2])),
"noise_only_blocks": len(noise_blocks), "noise_block_statistics": noise_statistics,
"split_minimum_true_strict": quantiles(part_min[is_true]),
"split_minimum_other_candidates": quantiles(part_min[~is_true]),
"strict_false_reduction": false_count / new_false,
"strict_false_reduction_fraction": 1 - new_false / false_count,
"first_stage_threshold_unchanged": detection.phy.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD,
"part_threshold": detection.part_threshold(2), "rf_tx": 0,
}
write_result("statistics.json", result)
# Научные статические рисунки сохраняются самостоятельными артефактами.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
figure, axes = plt.subplots(2, 2, figsize=(12, 8), constrained_layout=True)
axis = axes[0, 0]
bins = np.linspace(0, 1, 101)
axis.hist(false[:, 1], bins, histtype="step", label="Ложные строгие кандидаты", log=True)
axis.hist(main_scores, bins, histtype="step", label="Главный пик настоящей преамбулы", log=True)
axis.axvline(result["first_stage_threshold_unchanged"], color="gray", linestyle="--", label="Первый порог")
axis.set(xlabel="Нормированная корреляция", ylabel="Количество", title="Классификация после PHY/FEC/CRC")
axis.legend(fontsize=8)
axis = axes[0, 1]
for name, label in (("median", "Медиана"), ("mad", "MAD"), ("p99", "Квантиль 99%")):
axis.plot([block["start_sample"] / 8000000 for block in noise_blocks],
[block["score_statistics"][name] for block in noise_blocks], ".", markersize=2, label=label)
axis.set(xlabel="Время IQ, с", ylabel="Корреляционная статистика", title="Блоки чистого фона, определённые после RX")
axis.legend(fontsize=8)
axis = axes[1, 0]
axis.hist(part_min[~is_true], bins, histtype="step", log=True, label="Остальные локальные кандидаты")
axis.hist(part_min[is_true], bins, histtype="step", log=True, label="Кандидаты с последующим CRC PASS")
axis.axvline(detection.part_threshold(2), color="gray", linestyle="--", label="Порог каждой половины")
axis.set(xlabel="Меньшая корреляция двух половин", ylabel="Количество", title="Дешёвая проверка обеих частей")
axis.legend(fontsize=8)
axis = axes[1, 1]
rule_data = json.loads((OUTPUT / "rule_comparison.json").read_text(encoding="utf-8"))["rules"]
counts = [row["detector"]["strict_false_candidates"] for row in rule_data]
bars = axis.bar(["Группы", "1 часть", "2 части", "3 части"], counts)
axis.set_yscale("log")
for bar, count in zip(bars, counts):
axis.text(bar.get_x() + bar.get_width()/2, count*1.15, str(count), ha="center", fontsize=9)
axis.set(ylabel="Ложные строгие проверки", title="Все четыре варианта сохранили 240 пакетов")
figure.savefig(OUTPUT / "detector_distributions.png", dpi=160)
figure.savefig(OUTPUT / "detector_distributions.svg")
plt.close(figure)
return result
def run_functional_tests() -> None:
assert detection.part_threshold(2) < detection.part_threshold(3)
def main() -> None:
result = analyze()
print(json.dumps(result, ensure_ascii=False, indent=2))
print(json.dumps(measure_arrays(), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,374 @@
"""Lab052: воспроизводимый профиль обнаружителя, только программный IQ.
Диагностическая история принадлежит эксперименту. Приёмник не получает её
обратно; эталон используется только после независимых решений PHY/FEC/CRC.
"""
from __future__ import annotations
import argparse
import cProfile
from collections import Counter
from dataclasses import replace
import hashlib
import json
from pathlib import Path
import sys
import time
from types import SimpleNamespace
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import numpy as np
from PIL import Image
from experiments import lab051_streaming_receiver as previous
from experiments import lab051_receiver_performance as performance
from experiments.lab051_receiver_profile import process_memory
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "data/processed/lab052"
def write_result(name: str, value: dict) -> None:
OUTPUT.mkdir(parents=True, exist_ok=True)
(OUTPUT / name).write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def sequence_fixture():
"""Тот же генератор Lab051, готовность JPEG фиксируется первым замером.
Прежний опыт не сохранял encode_seconds, поэтому побитную историческую
временную сетку восстановить нельзя. Все новые сравнения используют одну
сохранённую сетку, один seed и те же байты JPEG, включая реальную encode cost.
"""
with Image.open(ROOT / "data/raw/lab011_scene.jpg") as source:
sequence = performance.video.encode_sequence(source, performance.video.PROFILES[2], 30)
path = OUTPUT / "source_fixture.json"
if path.exists():
fixture = json.loads(path.read_text(encoding="utf-8"))
for item, recorded in zip(sequence, fixture["images"], strict=True):
assert hashlib.sha256(item.jpeg).hexdigest() == recorded["sha256"]
return [replace(item, encode_seconds=recorded["encode_seconds"])
for item, recorded in zip(sequence, fixture["images"], strict=True)]
write_result(path.name, {"seed": 50050, "description": "Lab051 generator; frozen measured JPEG readiness",
"images": [{"image_id": item.image_id, "bytes": len(item.jpeg),
"encode_seconds": item.encode_seconds,
"sha256": hashlib.sha256(item.jpeg).hexdigest()} for item in sequence]})
return sequence
def quantiles(values) -> dict:
values = np.asarray(values, dtype=np.float64)
if not values.size:
return {"count": 0}
median = float(np.median(values))
return {"count": values.size, "min": float(values.min()), "median": median,
"mad": float(np.median(np.abs(values - median))),
"p90": float(np.quantile(values, .9)), "p99": float(np.quantile(values, .99)),
"p999": float(np.quantile(values, .999)), "max": float(values.max())}
class DetectionObserver:
"""Полная история только для конечного лабораторного профиля, вне RX."""
def __init__(self):
self.peaks = []
self.blocks = []
self.outcomes = []
self.cheap_rows = []
def scores(self, scores, base, low, high, decimation):
indexes = previous.offline._all_local_peak_indexes(scores)
offsets = base + indexes * decimation
selected = (offsets >= low) & (offsets < high)
self.peaks.append(np.column_stack((offsets[selected], scores[indexes[selected]])))
first = max(0, (low - base + decimation - 1) // decimation)
last = min(len(scores), (high - base + decimation - 1) // decimation)
self.blocks.append({"start_sample": low, "end_sample": high,
"score_statistics": quantiles(scores[first:last]),
"peak_statistics": quantiles(scores[indexes[selected]])})
def outcome(self, base, candidate, outcome):
self.outcomes.append((base + candidate.sample_offset, candidate.score,
base + outcome.refined_start_sample, int(outcome.accepted)))
def cheap(self, absolute, score, parts, passed):
self.cheap_rows.append([absolute, score, int(passed), *parts])
def save(self, name, result):
peaks = np.concatenate(self.peaks) if self.peaks else np.empty((0, 2))
outcomes = np.asarray(self.outcomes).reshape(-1, 4)
np.savez_compressed(OUTPUT / f"{name}_observations.npz", peaks=peaks, outcomes=outcomes)
if self.cheap_rows:
np.save(OUTPUT / f"{name}_cheap.npy", np.asarray(self.cheap_rows))
threshold = previous.phy.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD
valid = outcomes[:, 3] == 1
# Сначала строгий вердикт, затем классификация корреляционной статистики.
result["detection_profile"] = {
"total_local_peaks": len(peaks), "first_stage_candidates": int(np.sum(peaks[:, 1] >= threshold)),
"strict_candidates": len(outcomes), "strict_true_candidates": int(np.sum(valid)),
"strict_false_candidates": int(np.sum(~valid)),
"candidates_per_rf_second": len(outcomes) / result["iq_seconds"],
"false_candidates_per_rf_second": int(np.sum(~valid)) / result["iq_seconds"],
"all_local_peaks": quantiles(peaks[:, 1]),
"strict_true_scores": quantiles(outcomes[valid, 1]),
"strict_false_scores": quantiles(outcomes[~valid, 1]),
"blocks": self.blocks,
}
class ProfileBaseline(previous.StreamingReceiver):
"""Штатный Lab051 с наблюдением после корреляции и после решения."""
def __init__(self, observer):
super().__init__()
self.observer = observer
def _scan(self, owned_end):
front = previous.BlockFrontEnd(self.buffer, owned_end - self.base_sample, self.front_statistics,
decimator=self.decimator, base=self.base_sample)
original_scores = front.coarse_scores
def scores(samples, reference):
values = original_scores(samples, reference)
self.observer.scores(values, self.base_sample, self.scan_sample, owned_end, self.decimation)
return values
front.coarse_scores = scores
original_decode = previous.offline.decode_candidate_from_memmap
def decode(raw, count, candidate, number, **kwargs):
outcome = original_decode(raw, count, candidate, number, **kwargs)
self.observer.outcome(self.base_sample, candidate, outcome)
return outcome
try:
previous.offline.decode_candidate_from_memmap = decode
with previous.optimized_context(front):
return previous.previous.StreamingReceiver._scan(self, owned_end)
finally:
previous.offline.decode_candidate_from_memmap = original_decode
def measured_stream(sequence, factory):
"""Повторно использовать исходную событийную шкалу с фактическими push times."""
instances = []
def create():
receiver = factory()
instances.append(receiver)
return receiver
original = performance.fast
try:
performance.fast = SimpleNamespace(StreamingReceiver=create)
result = performance.run_stream(sequence)
finally:
performance.fast = original
receiver = instances[0]
result["coarse_seconds"] = receiver.coarse_seconds
result["strict_seconds"] = receiver.strict_seconds
result["peak_candidates_in_block"] = receiver.peak_candidate_count
if hasattr(receiver, "detection_statistics"):
result["detector"] = {**dict(receiver.detection_statistics),
"strict_true_candidates": receiver.packet_count,
"strict_false_candidates": receiver.candidate_count - receiver.packet_count,
"strict_checks_per_rf_second": receiver.candidate_count / result["iq_seconds"],
"cheap_seconds": receiver.cheap_seconds, "cluster_seconds": receiver.cluster_seconds,
"true_strict_seconds": receiver.true_strict_seconds,
"false_strict_seconds": receiver.false_strict_seconds,
"peak_cluster_count": receiver.peak_cluster_count,
"peak_eligible_count": receiver.peak_eligible_count,
"candidate_state_after_finish": receiver.candidate_state_count,
"detection_arrays_peak_estimate_bytes": receiver.working_detection_arrays_peak_bytes}
return result, receiver
def profile_baseline(sequence):
observer = DetectionObserver()
result, receiver = measured_stream(sequence, lambda: ProfileBaseline(observer))
observer.save("baseline", result)
result["timing_includes_diagnostic_observer"] = True
write_result("baseline_profile.json", result)
print(json.dumps({key: value for key, value in result.items() if key in (
"candidate_count", "received_images", "rx_physical_packets", "processing_per_rf_second",
"coarse_seconds", "strict_seconds")}), flush=True)
print(json.dumps({key: value for key, value in result["detection_profile"].items() if key != "blocks"}), flush=True)
def unprofiled_baseline(sequence):
result, receiver = measured_stream(sequence, previous.StreamingReceiver)
result["timing_includes_diagnostic_observer"] = False
write_result("baseline_stream.json", result)
print(json.dumps({key: result[key] for key in ("candidate_count", "processing_seconds",
"processing_per_rf_second", "rx_physical_packets", "received_images", "object_jpeg_integrity")}), flush=True)
def run_functional_tests() -> None:
assert quantiles([1, 2, 3])["median"] == 2
def compare_rules(sequence):
from experiments import lab052_candidate_detection as detection
baseline = json.loads((OUTPUT / "baseline_profile.json").read_text(encoding="utf-8"))
results = []
for parts in (0, 1, 2, 3):
observer = DetectionObserver()
rule = detection.DetectorRule(parts=parts)
result, receiver = measured_stream(sequence, lambda: detection.StreamingReceiver(rule=rule, observer=observer))
observer.save(f"parts{parts}", result)
result["rule"] = {"parts": parts, "width_samples": rule.cluster_width_samples,
"part_threshold": detection.part_threshold(parts) if parts else None}
result["true_candidates_lost"] = baseline["rx_physical_packets"] - result["rx_physical_packets"]
false_count = result["detector"]["strict_false_candidates"]
baseline_false = baseline["detection_profile"]["strict_false_candidates"]
result["false_candidate_reduction"] = baseline_false / false_count if false_count else None
result["false_candidate_reduction_fraction"] = 1 - false_count / baseline_false
result["timing_includes_diagnostic_observer"] = True
write_result(f"parts{parts}.json", result)
compact = {key: result[key] for key in ("rule", "received_images", "object_jpeg_integrity",
"true_candidates_lost", "false_candidate_reduction", "detector",
"processing_per_rf_second", "fresh_display_fps")}
results.append(compact)
print(json.dumps(compact), flush=True)
write_result("rule_comparison.json", {"rules": results, "rf_tx": 0})
def optimized_stream(sequence):
from experiments import lab052_candidate_detection as detection
results = []
for number in range(1, 4):
result, receiver = measured_stream(sequence, detection.StreamingReceiver)
result["timing_includes_diagnostic_observer"] = False
write_result(f"stream_repeat_{number}.json", result)
results.append(result)
print(json.dumps({"repeat": number, "ratio": result["processing_per_rf_second"],
"fresh_fps": result["fresh_display_fps"], "packets": result["rx_physical_packets"]}), flush=True)
result = sorted(results, key=lambda row: row["processing_seconds"])[1]
result["selection"] = "median processing time of three complete measured passes; one whole timeline"
write_result("stream.json", result)
print(json.dumps({key: value for key, value in result.items() if key not in (
"blocks", "images", "rss_samples", "fresh_image_age_seconds")}), flush=True)
def profile_cpu(sequence):
from experiments import lab052_candidate_detection as detection
import pstats
from io import StringIO
profiler = cProfile.Profile()
class ProfiledReceiver(detection.StreamingReceiver):
def push_iq_block(self, samples):
return profiler.runcall(super().push_iq_block, samples)
def finish(self):
return profiler.runcall(super().finish)
result, receiver = measured_stream(sequence, ProfiledReceiver)
profiler.dump_stats(str(OUTPUT / "receiver_cpu.prof"))
report = StringIO()
stats = pstats.Stats(profiler, stream=report).strip_dirs()
stats.sort_stats("tottime").print_stats(30)
stats.sort_stats("cumtime").print_stats(30)
(OUTPUT / "receiver_cpu.txt").write_text(report.getvalue(), encoding="utf-8")
result["timing_includes_cprofile"] = True
write_result("cpu_profile.json", result)
print(report.getvalue(), flush=True)
def long_state_test(sequence=None, block_count=2048):
"""16,78 с распределённого шума, меняющаяся мощность, без истории в RX."""
from experiments import lab052_candidate_detection as detection
receiver = detection.StreamingReceiver()
random_generator = np.random.default_rng(52030)
checkpoints = []
accepted = 0
for number in range(block_count):
components = random_generator.standard_normal((2, receiver.block_samples), dtype=np.float32)
scale = (.002, .2, 20., .02)[(number // 257) % 4]
values = (components[0] + 1j * components[1]) * scale
accepted += len(receiver.push_iq_block(values))
assert receiver.buffer.size <= receiver.buffer_limit_samples
assert receiver.candidate_state_count == 0
if (number + 1) % 256 == 0:
checkpoints.append({"block": number + 1, "iq_bytes": receiver.buffer.nbytes,
"decimated_bytes": receiver.decimator.values.nbytes,
"retained_candidates": receiver.candidate_state_count,
"peak_eligible_candidates": receiver.peak_eligible_count,
"counter_keys": len(receiver.detection_statistics) + len(receiver.rejected_reasons),
**process_memory()})
accepted += len(receiver.finish())
result = {"blocks": block_count, "iq_seconds": block_count * receiver.block_samples / 8000000,
"background_scales": [.002, .2, 20, .02], "accepted_noise_packets": accepted,
"peak_retained_iq_bytes": receiver.peak_buffer_samples * 8,
"iq_limit_bytes": receiver.buffer_limit_samples * 8,
"retained_after_finish_bytes": receiver.buffer.nbytes + receiver.decimator.values.nbytes,
"constant_template_bytes": receiver.raw_reference.nbytes + receiver.matched_taps.nbytes + receiver.preamble_bits.nbytes,
"candidate_state_after_finish": receiver.candidate_state_count,
"peak_eligible_candidates": receiver.peak_eligible_count,
"peak_cluster_count": receiver.peak_cluster_count,
"detector": dict(receiver.detection_statistics), "strict_candidates": receiver.candidate_count,
"processing_seconds": receiver.processing_seconds, "checkpoints": checkpoints,
"process_memory": process_memory(), "rf_tx": 0}
write_result("memory.json", result)
print(json.dumps(result), flush=True)
def sensitivity_test(sequence=None):
"""До/после на одинаковом IQ около границы работоспособности FEC."""
from experiments import lab052_candidate_detection as detection
physical = previous.phy.lab045
payload = bytes(range(256))
plan = physical.build_transmission_plan(payload)
raw = previous.phy.build_parameterized_tx_cs8(plan, previous.phy.PhyMode(500000))
components = np.frombuffer(raw, np.int8).astype(np.float32)
clean = components[0::2] + 1j * components[1::2]
energy = float(np.sum(np.abs(clean) ** 2)) / plan.frame_symbol_count
rows = []
for ebn0 in (0, 1, 2, 3, 7):
for cfo in (-100, 10, 100):
for seed, scale in ((52031, .002), (52032, .2), (52033, 1.)):
random_generator = np.random.default_rng(seed)
sigma = np.sqrt(energy / (2 * 10 ** (ebn0 / 10)))
phase = .73 + (seed % 2) * np.pi
values = clean * np.exp(1j * (phase + 2*np.pi*cfo*np.arange(len(clean))/8000000))
values += sigma * (random_generator.normal(size=len(clean)) + 1j*random_generator.normal(size=len(clean)))
samples = np.concatenate((np.zeros(63217, np.complex64), (values * scale).astype(np.complex64),
np.zeros(350000, np.complex64)))
outcomes = []
for constructor in (previous.StreamingReceiver, detection.StreamingReceiver):
receiver = constructor()
packets = []
for start in range(0, len(samples), receiver.block_samples):
packets.extend(receiver.push_iq_block(samples[start:start + receiver.block_samples]))
packets.extend(receiver.finish())
# Оценка содержимого только после завершения независимого RX.
outcomes.append({"accepted": len(packets),
"payload_match": len(packets) == 1 and packets[0].payload == payload,
"reasons": dict(receiver.rejected_reasons)})
rows.append({"ebn0_db": ebn0, "cfo_hz": cfo, "seed": seed, "amplitude_scale": scale,
"baseline": outcomes[0], "optimized": outcomes[1],
"lost_baseline_packet": outcomes[0]["payload_match"] and not outcomes[1]["payload_match"]})
result = {"cases": rows, "count": len(rows),
"baseline_correct": sum(row["baseline"]["payload_match"] for row in rows),
"optimized_correct": sum(row["optimized"]["payload_match"] for row in rows),
"lost_baseline_packets": sum(row["lost_baseline_packet"] for row in rows), "rf_tx": 0}
write_result("sensitivity.json", result)
print(json.dumps({key: value for key, value in result.items() if key != "cases"}), flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--stage", choices=("profile", "baseline", "rules", "stream", "cpu", "memory", "sensitivity"), default="profile")
arguments = parser.parse_args()
OUTPUT.mkdir(parents=True, exist_ok=True)
sequence = sequence_fixture()
print(f"Lab052 {arguments.stage}: START; RF TX=0", flush=True)
{"profile": profile_baseline, "baseline": unprofiled_baseline, "rules": compare_rules, "stream": optimized_stream,
"cpu": profile_cpu, "memory": long_state_test, "sensitivity": sensitivity_test}[arguments.stage](sequence)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,211 @@
"""Свойства локального обнаружителя Lab052 без оборудования и внешних данных."""
from __future__ import annotations
import numpy as np
import pytest
from types import SimpleNamespace
from experiments import lab052_candidate_detection as lab
from experiments.lab050_streaming_receiver import streaming_mode
def waveform(payload: bytes, *, seed: int = 52001, ebn0_db: float = 7, phase: float = .73,
cfo_hz: float = 10, scale: float = 1) -> np.ndarray:
random_generator = np.random.default_rng(seed)
# Генератор Lab045 ограничен payload=256, но приёмный PHY допускает
# packet 12..1024. Собираем короткий кадр штатными кодерами для границ.
physical = lab.phy.lab045
packet = physical.build_packet(payload, physical.MESSAGE_TYPE, physical.SEQUENCE_NUMBER)
coded = physical.conv.encode(physical.lab044.bytes_to_bits(packet))
permutation = physical.interleaver.build_permutation(len(coded), physical.INTERLEAVER_COLUMNS)
bits = np.concatenate((physical.lab044.preamble_bits(), physical.encode_phy_header(len(packet)),
physical.interleaver.interleave(coded, permutation)))
plan = SimpleNamespace(frame_bits=bits, frame_symbol_count=len(bits))
raw = lab.phy.build_parameterized_tx_cs8(plan, lab.phy.PhyMode(500000))
components = np.frombuffer(raw, np.int8).astype(np.float32)
values = components[0::2] + 1j * components[1::2]
values *= np.exp(1j * (phase + 2 * np.pi * cfo_hz * np.arange(len(values)) / 8000000)).astype(np.complex64)
energy = float(np.sum(abs(values) ** 2)) / plan.frame_symbol_count
sigma = np.sqrt(energy / (2 * 10 ** (ebn0_db / 10)))
values += sigma * (random_generator.normal(size=len(values)) + 1j * random_generator.normal(size=len(values)))
return (values * scale).astype(np.complex64)
def decode(samples, receiver):
packets = []
for start in range(0, len(samples), receiver.block_samples):
packets.extend(receiver.push_iq_block(samples[start:start + receiver.block_samples]))
assert receiver.candidate_state_count == 0 if hasattr(receiver, "candidate_state_count") else True
packets.extend(receiver.finish())
return packets
def test_cluster_of_false_peaks_has_one_strongest_representative():
scores = np.zeros(1600, np.float32)
scores[[600, 620, 654, 700, 730]] = [.05, .07, .12, .06, .04]
assert lab.cluster_peak_indexes(scores, 256, .03).tolist() == [654]
def test_two_minimum_frames_do_not_compete_even_at_opposite_coarse_errors():
distance = lab.MIN_FRAME_SAMPLES - 2 * lab.COARSE_ERROR_SAMPLES
scores = np.zeros(8000, np.float32)
indexes = [1000, 1000 + distance // 8]
scores[indexes] = [.3, .9]
assert lab.cluster_peak_indexes(scores, lab.CLUSTER_WIDTH_SAMPLES // 8, .03).tolist() == indexes
def test_equal_peaks_use_reproducible_local_tie():
scores = np.zeros(1000, np.float32)
scores[[100, 120, 600]] = .2
assert lab.cluster_peak_indexes(scores, 100, .03).tolist() == [120, 600]
def test_equal_peak_chain_does_not_extend_cluster_width():
scores = np.zeros(500, np.float32)
scores[[100, 180, 260]] = .2
assert lab.cluster_peak_indexes(scores, 100, .03).tolist() == [180, 260]
def test_single_loud_half_cannot_pass_split_check():
random_generator = np.random.default_rng(52002)
reference = random_generator.choice([-1., 1.], 2560).astype(np.complex64)
samples = (random_generator.normal(size=2560) + 1j * random_generator.normal(size=2560)).astype(np.complex64)
samples[:1280] = reference[:1280] * 100
assert lab.part_correlations(samples, reference, 1)[0] > lab.part_threshold(1)
assert np.any(lab.part_correlations(samples, reference, 2) < lab.part_threshold(2))
@pytest.mark.parametrize("phase,scale", [(0, .001), (.73, 1), (np.pi, 100), (-1.6, .1)])
def test_true_preamble_preserves_unknown_gain_phase_and_polarity(phase, scale):
random_generator = np.random.default_rng(52003)
reference = random_generator.choice([-1., 1.], 2560).astype(np.complex64)
samples = reference * np.exp(1j * phase) * scale
assert np.all(lab.part_correlations(samples, reference, 2) >= lab.part_threshold(2))
def test_noise_is_rejected_before_strict_timing(monkeypatch):
random_generator = np.random.default_rng(52004)
samples = (random_generator.normal(size=750000) + 1j * random_generator.normal(size=750000)).astype(np.complex64)
def forbidden(*args, **kwargs):
pytest.fail("Чистый шум этого seed не должен достигнуть строгого тракта")
monkeypatch.setattr(lab.offline, "decode_candidate_from_memmap", forbidden)
receiver = lab.StreamingReceiver()
assert decode(samples, receiver) == []
assert receiver.detection_statistics["cheap_stage_rejected"] > 0
assert receiver.front_statistics["unique_soft_windows"] == 0
@pytest.mark.parametrize("ebn0_db,scale,cfo", [(7, 1, 10), (2, .02, -100), (1, .2, 100)])
def test_weak_true_packet_matches_lab051(ebn0_db, scale, cfo):
payload = bytes(range(128))
values = waveform(payload, seed=52005, ebn0_db=ebn0_db, scale=scale, cfo_hz=cfo)
samples = np.concatenate((np.zeros(63217, np.complex64), values, np.zeros(350000, np.complex64)))
baseline = decode(samples, lab.previous.StreamingReceiver())
optimized = decode(samples, lab.StreamingReceiver())
assert [packet.payload for packet in optimized] == [packet.payload for packet in baseline]
assert [packet.payload for packet in baseline] == ([payload] if ebn0_db >= 2 else [])
@pytest.mark.parametrize("block_samples,prefix", [(32749, 32749 - 3), (65536, 65536 - 8),
(65536, 65536 + 7)])
def test_minimum_adjacent_frames_at_block_boundary_are_both_decoded(block_samples, prefix):
# Минимальный packet: пустой payload, заголовок+CRC=12 байт.
first = waveform(b"", seed=52006, phase=.4)
second = waveform(b"x", seed=52007, phase=-2)
assert len(first) == lab.MIN_FRAME_SAMPLES
samples = np.concatenate((np.zeros(prefix, np.complex64), first, second,
np.zeros(350000, np.complex64)))
receiver = lab.StreamingReceiver(block_samples=block_samples)
assert [packet.payload for packet in decode(samples, receiver)] == [b"", b"x"]
def test_changing_background_with_weak_packet():
random_generator = np.random.default_rng(52008)
samples = (random_generator.normal(size=700000) + 1j * random_generator.normal(size=700000)).astype(np.complex64)
samples[:180000] *= .001
samples[180000:300000] *= 20
samples[300000:] *= .03
payload = b"varying local background"
values = waveform(payload, seed=52009, ebn0_db=2, scale=.02)
samples[370013:370013 + len(values)] = values
assert [packet.payload for packet in decode(samples, lab.StreamingReceiver())] == [payload]
@pytest.mark.parametrize("true_start", [30000, 65536 - 8, 65536 + 7])
def test_false_strong_preamble_does_not_hide_weak_valid_packet(true_start):
decoy_start = true_start - 20000
samples = np.zeros(700000, np.complex64)
decoy = waveform(b"", seed=52022, ebn0_db=100)[:lab.PREAMBLE_SAMPLES + 512]
payload = b"actual valid short packet"
true = waveform(payload, seed=52021, ebn0_db=7, scale=.2)
samples[decoy_start:decoy_start + len(decoy)] += decoy
samples[true_start:true_start + len(true)] += true
expected = decode(samples, lab.previous.StreamingReceiver())
actual = decode(samples, lab.StreamingReceiver())
assert [packet.payload for packet in expected] == [payload]
assert [(packet.start_sample, packet.payload) for packet in actual] == [
(packet.start_sample, packet.payload) for packet in expected]
def test_constant_templates_are_exact_and_restored_on_exception():
receiver = lab.StreamingReceiver()
owner = lab.phy.lab045.lab044
originals = (owner._preamble_reference_samples, owner.preamble_bits, lab.previous.lab043._matched_filter_taps)
with streaming_mode(lab.phy.PhyMode(500000)):
np.testing.assert_array_equal(receiver.raw_reference, originals[0]())
np.testing.assert_array_equal(receiver.preamble_bits, originals[1]())
np.testing.assert_array_equal(receiver.matched_taps, originals[2]())
with pytest.raises(RuntimeError):
with receiver.constant_templates():
raise RuntimeError("restore templates")
assert originals == (owner._preamble_reference_samples, owner.preamble_bits, lab.previous.lab043._matched_filter_taps)
@pytest.mark.parametrize("count", [1, 2, 3])
def test_shared_split_scores_equal_individual_vdot(count):
random_generator = np.random.default_rng(52024)
values = (random_generator.normal(size=9000) + 1j * random_generator.normal(size=9000)).astype(np.complex64)
values[4000:] *= .003
reference = random_generator.choice([-1., 1.], size=2560).astype(np.complex64)
front = object.__new__(lab.CandidateFrontEnd)
front.detection_values = values
scores = front.split_scores(reference, count, len(values) - len(reference) + 1)
for index in (0, 1001, 3999, len(scores) - 1):
np.testing.assert_allclose(scores[index], lab.part_correlations(values[index:index+len(reference)], reference, count),
atol=2e-6, rtol=2e-5)
def test_bounded_candidate_state_and_finish():
random_generator = np.random.default_rng(52010)
receiver = lab.StreamingReceiver()
samples = (random_generator.normal(size=65536) + 1j * random_generator.normal(size=65536)).astype(np.complex64)
for index in range(120):
receiver.push_iq_block(samples * (.01 if index % 2 else 10))
assert receiver.candidate_state_count == 0
assert receiver.buffer.size <= receiver.buffer_limit_samples
assert receiver.decimator.values.size <= (receiver.buffer_limit_samples + 7) // 8
receiver.finish()
assert receiver.buffer.size == receiver.decimator.values.size == receiver.candidate_state_count == 0
assert receiver.peak_cluster_count <= 20
assert receiver.observer is None
def test_20k_does_not_enter_new_filter_and_mode_is_restored():
old = lab.previous.lab045.SYMBOL_RATE
with pytest.raises(ValueError):
lab.StreamingReceiver(20000)
lab.run_functional_tests()
assert lab.previous.lab045.SYMBOL_RATE == old
with streaming_mode(lab.phy.PhyMode(20000)):
assert lab.previous.lab045.SAMPLES_PER_SYMBOL == 400
assert lab.previous.lab045.SYMBOL_RATE == old
@pytest.mark.parametrize("rule", [dict(parts=4), dict(parts=-1), dict(cluster_width_samples=26720),
dict(cluster_width_samples=3)])
def test_invalid_rule_rejected(rule):
with pytest.raises(ValueError):
lab.DetectorRule(**rule)