Compare commits
3 Commits
cdf5396e80
...
cc38900cae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc38900cae | ||
|
|
9f33607d3a | ||
|
|
60691a09f1 |
362
PROJECT_LOG.md
362
PROJECT_LOG.md
@@ -1641,3 +1641,365 @@ Git add/commit/push/pull/fetch не выполнялись.
|
||||
и проверкой выполнимости 5 кадров/с. Сопряжение CRSF и его сценарии отказов
|
||||
требуют отдельной постановки и уточнения аппаратуры. Нового RF, QPSK,
|
||||
CRSF/ELRS-протокола, перекодирования JPEG или переноса на 180 МГц здесь нет.
|
||||
|
||||
# Запись 039
|
||||
|
||||
## Дата
|
||||
|
||||
8 сентября 2026 года.
|
||||
|
||||
## Тема
|
||||
|
||||
Lab050: потоковая архитектура JPEG, ограничение возраста и блочный IQ-приём.
|
||||
|
||||
## Выполнено
|
||||
|
||||
По явному заданию проверены HEAD, status, diff/stat/check и staged scope Lab049.
|
||||
Создан коммит cdf5396e807b83f82a264632dc46f71428fcd616 с сообщением
|
||||
Lab049: define ELRS control and SDR video architecture. Вошли 12 файлов:
|
||||
код, тесты, документы, журнал, индекс и указатель README. Сгенерированные данные,
|
||||
настройки окружения и посторонние локальные файлы не добавлялись. Push не выполнялся.
|
||||
Lab050 начата от нового HEAD и оставлена без staging/commit.
|
||||
|
||||
Аудит Lab034–037 подтвердил возможность переиспользовать существующий
|
||||
protocol/video_frame_scheduler.py: активное изображение завершается,
|
||||
ожидающее заменяется самым свежим, перед началом прогнозируется завершение.
|
||||
Добавлены готовность JPEG отдельно от времени съёмки, произвольный MAX_VIDEO_AGE,
|
||||
прогноз задержки после RF и счётчик максимальной ожидающей очереди.
|
||||
Пять прежних политик сохраняют поведение на проверенном сценарии видео+CONTROL.
|
||||
В самой Lab050 SDR-очередь содержит только VIDEO; управление ELRS отдельно.
|
||||
|
||||
Verifier/adversary воспроизвели ошибку первой реализации: позднее завершение
|
||||
кодирования старого снимка вытесняло более свежий, либо возвращало старый снимок
|
||||
после опустошения очереди. Исправлено сравнением времени съёмки и ID с сохранением
|
||||
самого свежего виденного значения. Две регрессии и повторная независимая проверка PASS.
|
||||
|
||||
Из data/raw/lab011_scene.jpg созданы четыре фиксированных профиля:
|
||||
A 320×240 RGB q35 — 8724 байта; B 160×120 RGB q35 — 2771;
|
||||
C 128×96 RGB q25 — 1740; D 160×120 grayscale q30 — 2028.
|
||||
Первый A совпал с SHA Lab047. По 30 детерминированных сдвигов фото, не съёмка камеры;
|
||||
размеры соответственно 8724–9250, 2771–3031, 1740–1893, 2028–2233 байта.
|
||||
PSNR относительно несжатого изображения того же профиля:
|
||||
28,12 / 28,11 / 27,17 / 30,15 dB. Это не оценка пригодности для управления.
|
||||
|
||||
Нынешний пакет несёт 244 байта JPEG и занимает 5766 символьных интервалов.
|
||||
Точный предел Jmax=244*floor(Rs/(5766*fps)) включает фрагментацию, добивку,
|
||||
FEC, PHY header, преамбулу, защитные интервалы и фильтр.
|
||||
На 500/800/1000 ксимв/с для 5 кадров/с предел 4148/6588/8296 байт;
|
||||
для 10 — 1952/3172/4148 байт. BPSK, FEC R=1/2 K=7, CRC и форматы сохранены.
|
||||
QPSK, новый кодек и 12+3 стирающий FEC не вводились.
|
||||
|
||||
Выполнены 240 событийных сценариев: четыре профиля, вход 3/5/10 кадров/с,
|
||||
Rs=200/400/500/800/1000 ксимв/с при Fs=8 Мвыб/с, предел возраста
|
||||
200/333/500/1000 мс. Ожидающая очередь не превышает один снимок.
|
||||
Сохраняются capture/encode/queue/TX/RX/complete/decode/display timestamps;
|
||||
не выполненные стадии имеют null. FPS внутри окна и после опустошения разделены.
|
||||
|
||||
Идеальная модель C/500k/5fps/200ms передаёт и показывает 30/30 за 6 с,
|
||||
без удалений, FRESH DISPLAY FPS=5, возраст min/median/max
|
||||
92,563 / 92,590 / 92,629 мс. Это модель нулевой стоимости RX и экрана.
|
||||
При C/200k, камере 10 кадров/с и возрасте 333 мс передано 14, заменено 16;
|
||||
внутри 3 с показано 13, FRESH DISPLAY FPS=4,333, ещё один после окна.
|
||||
При возрасте 200 мс все 30 отклонены: радио само требует 230,64 мс.
|
||||
|
||||
Добавлен StreamingReceiver.push_iq_block с выдачей пакетов до finish.
|
||||
Корреляция и строгий PHY/FEC/CRC взяты из Lab048; в старый декодер добавлен
|
||||
необязательный IQ-reader без изменения поведения по умолчанию.
|
||||
Буфер удерживает блоки, левый запас и максимальный допустимый PHY-кадр;
|
||||
TX-границы, payload, ожидаемые ID и число кадров RX не получает.
|
||||
Децимация поиска 20/10/8/5/4 сохраняет две выборки/символ, порог Lab047
|
||||
0.030964473262429237 не меняется. Адаптер последовательный, live-драйвер не подключён.
|
||||
|
||||
На каждой скорости выполнен шумный опыт трёх JPEG C, 24 RF-пакета:
|
||||
единый coded-bit Eb/N0=7 dB, seed=50050, CFO +10 Гц, фаза 0,73 рад, смена полярности.
|
||||
Все пять опытов дали 24/24 пакета и 3/3 точных JPEG до finish.
|
||||
Большой IQ-файл не создавался; SHA проверялся после независимого CRC/сборки.
|
||||
Обработка заняла 35,12417 / 59,47086 / 55,15755 / 60,95497 / 55,92195 с
|
||||
при IQ 0,792483 / 0,401868 / 0,323745 / 0,206561 / 0,167499 с.
|
||||
Отношения 44,32166 / 147,98602 / 170,37365 / 295,09484 / 333,86514:
|
||||
приёмник не успевает в реальном времени. На 500k строгая обработка 54,586 с
|
||||
против 0,492 с грубого поиска, 2374 кандидата и 24 принятых пакета.
|
||||
После исправления вызовов API packet в модели итоговая матрица пересчитана
|
||||
через --reuse-benchmarks; пять тяжёлых замеров сохранены без повторения.
|
||||
|
||||
Пик удерживаемых IQ 2,13–6,51 МиБ; расчёт массивов с запасом 47,5–143,4 МиБ,
|
||||
не RSS процесса. Предел IQ-буфера не зависит от длительности сессии.
|
||||
История событий симулятора хранится для отчёта и не выдаётся за постоянную
|
||||
память бесконечной камеры. Потери блоков/переполнение live-драйвера не реализованы.
|
||||
|
||||
При подстановке измеренной стоимости RX все 30 кадров C/5fps отклоняются до TX,
|
||||
FRESH DISPLAY FPS=0 для всех проверенных пределов возраста.
|
||||
Условный вариант: JPEG ≤1952 байт, 500 ксимв/с, MAX_VIDEO_AGE=200 мс.
|
||||
При стоимости RX-пакета ≤16 мс модель даёт 5 кадров/с и возраст около 192,5 мс;
|
||||
это требует примерно 144-кратного ускорения данного замера.
|
||||
Для 10 кадров/с нужны ≤12 мс и примерно 192-кратное ускорение; возраст около 160,5 мс,
|
||||
29 показов в первые 3 с (9,667 кадра/с), ещё один после окна, интервалы около 10 кадров/с.
|
||||
Ускорения не достигнуты и не обещаются. Обе цели без QPSK — CONDITIONAL;
|
||||
нынешний RX — NO по реальному времени. Камера, экран, jitter и запас ещё не измерены.
|
||||
|
||||
Добавлены experiments/lab050_video_stream.py, lab050_streaming_receiver.py,
|
||||
tests/test_lab050_video_stream.py, docs/lab050_current_state.md и локальные
|
||||
результаты data/processed/lab050. Обновлён LABS_INDEX. Модули аварии,
|
||||
управления и сеансов не менялись; при потере ELRS видео не продлевает движение.
|
||||
|
||||
44 целевых теста PASS. Verifier независимо проверил границы JPEG, 60 сценариев
|
||||
очереди, 32 прогноза RX, исходные IQ-числа и все итоговые временные отметки JSON.
|
||||
Adversary подтвердил устранение найденной регрессии свежести.
|
||||
quick_gate PASS: 650 тестов и 14 функциональных проверок.
|
||||
pytest: 650 passed за 78,73 с, одно предупреждение о доступе к .pytest_cache.
|
||||
git diff --check PASS. RF TX=0, оборудование не запускалось.
|
||||
Lab050 git add/commit и push/pull/fetch не выполнялись.
|
||||
|
||||
Следующий этап: STOP. Рекомендуемый Lab051 — программное профилирование
|
||||
и ускорение строгой проверки ложных кандидатов на C/500k с сохранением
|
||||
независимости RX и FEC/CRC. До доказанной скорости и свежести не выполнять
|
||||
RF, QPSK, новый видеокодек или перенос SDR на 180 МГц.
|
||||
|
||||
# Запись 040
|
||||
|
||||
## Дата
|
||||
|
||||
8 сентября 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Lab051 — алгоритмическое ускорение потокового BPSK-приёмника на 500 ксимв/с
|
||||
и проверка свежести JPEG при измеренной стоимости обработки
|
||||
|
||||
## Выполнено
|
||||
|
||||
Перед новой разработкой проверены HEAD, status, diff stat и diff check.
|
||||
По прямому разрешению текущего задания Lab050 закреплена коммитом
|
||||
60691a09f193f36983c7174c5252ed0897c559e9:
|
||||
Lab050: model fresh-frame SDR video stream.
|
||||
В показанный staged scope вошли девять файлов: код и тесты Lab050, необходимые
|
||||
изменения прежних модулей, отчёт, журнал, индекс и небольшой receiver_benchmarks.json.
|
||||
Большой result.json, JPEG-профили и посторонние файлы не добавлялись. Push не выполнялся.
|
||||
|
||||
Lab051 начата от нового HEAD. Аудит прочитал Lab046B/C, Lab047/048/050,
|
||||
связанные функции Lab043/044/045 и convolutional_fec. Рабочая точка строго
|
||||
500000 символов/с, Fs=8000000, 16 отсчётов/символ; профиль C 128×96 RGB q25,
|
||||
1740–1893 байта. Управление ELRS/CRSF и локальная защита остаются отдельно.
|
||||
Ни один файл protocol в Lab051 не менялся.
|
||||
|
||||
Воспроизведён прежний короткий шумный опыт Lab050: три JPEG, 24 пакета,
|
||||
seed50050, coded-bit Eb/N0=7 дБ, CFO+10 Гц, фаза0,73 и чередование полярности.
|
||||
Историческая стоимость 55,157553 с / 0,323745 с IQ =170,373647.
|
||||
Свежий инструментированный baseline занял49,614327 с, отношение153,251430;
|
||||
это отдельный замер, с влиянием настольной нагрузки/инструментации.
|
||||
Обнаружены2374 кандидата:24 valid и2350 false; средние72,967/20,099 мс.
|
||||
|
||||
Главная стоимость ложных кандидатов — timing:41,833 с из47,232 с.
|
||||
При nominal416 и диапазоне±800 прежняя функция возвращалась к Python-перебору
|
||||
1601 смещения. Matched filter на ложных кандидатах занимал0,934 с, уточнение2,797 с.
|
||||
Ранний отказ PHY header уже существовал и не запускал тело/Viterbi/CRC.
|
||||
|
||||
Добавлен отдельный вычислительный адаптер Lab051. Повторно используется внутренняя
|
||||
часть децимации с точным совпадением отсчётов; пересчитываются новые края.
|
||||
Общая корреляция уточнения и общий перенос/RRC обслуживают срезы кандидатов.
|
||||
Сохранены локальный DC и нулевые края. Результат извлечения кэшируется по
|
||||
уточнённому положению/длине в пределах блока. Timing вычисляет все прежние1601
|
||||
смещения через корреляцию и накопленные энергии, без сужения диапазона.
|
||||
Для неразделимых FFT-пиков и постоянного IQ сохранён исходный выбор первого максимума.
|
||||
|
||||
Viterbi использует предвычисленные четыре пары евклидовых метрик и сравнение
|
||||
двух ветвей с прежним tie→branch0. K7,R1/2,171/133,tail6,интерливер,CRC и
|
||||
результаты декодирования сохранены. CFO-оценка прежняя; кэшируются векторы переноса.
|
||||
Новый порог отсева, GPU, multiprocessing и зависимости не добавлены.
|
||||
Фиксированный порог кандидатов0.030964473262429237 не изменялся.
|
||||
|
||||
Финальный короткий контроль:24/24 пакета,3/3 JPEG, SHA MATCH; совпали число
|
||||
кандидатов и все причины отказов. Медиана трёх проходов1,144176 с,
|
||||
processing/RF3,534193 — ускорение48,21 раза относительно исторической базы.
|
||||
Средний valid candidate17,7675 мс, false0,187317 мс; false-медиана0,0605 мс.
|
||||
Полная стоимость с ложными кандидатами/фоном47,674 мс на физический пакет.
|
||||
|
||||
Основной опыт:30 изображений, вход5/с, окно6 с, LATEST_ONLY, MAX_VIDEO_AGE200 мс.
|
||||
Реально поданы6,044416 с синтетического IQ блоками; receiver не получал TX-границы,
|
||||
payload или ожидаемый счётчик. Все240/240 пакетов и30/30 JPEG восстановлены точно.
|
||||
Фактическая стоимость каждого push, сборки и JPEG decode использована в событийной
|
||||
шкале CPU. Суммарно38,630132 с, processing/RF6,391045,160,959 мс/пакет.
|
||||
Получены147082 кандидата,12239 новых окон синхронизации и135057 cache hits.
|
||||
Ни один JPEG не уложился в200 мс: DISPLAYED=FRESH DISPLAYED=0,FRESH DISPLAY FPS=0.
|
||||
Цель5 свежих/с и минимальная цель>0 НЕ достигнуты.
|
||||
|
||||
Переданы30 изображений, до TX удалено0, после decode по возрасту удалено30.
|
||||
Возраст декодированных/отброшенных JPEG min/median/max:
|
||||
276,515/15691,203/31150,009 мс. Это не измерение камеры/экрана: их задержки UNKNOWN.
|
||||
Capture lookup используется только после независимого JPEG для отчётной метрики;
|
||||
в прежнем fragment format нет capture timestamp. Live-контроль возраста не доказан.
|
||||
Видеочередь≤1, однако непрерывный IQ-источник потребовал бы внешнего backlog
|
||||
2088473848 байт при отставании32,632404 с. Такой буфер не реализован и не скрыт.
|
||||
REAL-TIME CAPABLE=NO. Поток воспроизводится офлайн с обратным давлением источнику.
|
||||
|
||||
Пик удерживаемого IQ3177728 байт(3,031 МиБ), проверяемый предел3398912 байт.
|
||||
Инженерная оценка рабочих массивов136,161 МиБ; это не универсальный предел и не RSS.
|
||||
RSS процесса после потока154,105 МиБ, peak178,004 МиБ, включая Python/генератор/кэши.
|
||||
Длинный тест4096 блоков(33,554432 с,нулевой фон) не показал роста IQ и децимации:
|
||||
2653440 и397216 байт после прогрева. RSS156,199–156,250 МиБ. Finish освобождает буферы.
|
||||
В отдельном replay посчитана нижняя граница наблюдаемых ndarray/владельцев и
|
||||
tracemalloc peak. Внутренние C-аллокации не объявляются полностью измеренными;
|
||||
холодные общие массивы могут занимать больше памяти, чем отдельное старое окно.
|
||||
|
||||
Сохранённая Lab047-запись20 ксимв/с повторно разобрана штатным Lab048:
|
||||
36/36,FEC/CRC PASS,JPEG SHA MATCH,19 ошибок до FEC и0 после. Исходные IQ/JPEG сохранены.
|
||||
Новый адаптер рассчитан только на500 ксимв/с; аппаратных запусков не было.
|
||||
|
||||
38 целевых тестов PASS. Verifier независимо нашёл два FFT tie-расхождения;
|
||||
оба исправлены и повторно воспроизведены с совпадением старого результата.
|
||||
Adversary проверил malformed PHY без body/Viterbi, дубликаты/старые ID,
|
||||
лимиты объекта, усечение, восстановление подмен и арифметику возраста/backlog.
|
||||
Таблицы отчёта сверены с JSON. Документ проверен в UTF-8, ссылки существуют.
|
||||
quick_gate PASS:688 тестов,17 функциональных проверок,13 штатных пропусков.
|
||||
Полный pytest:688 passed за72,92 с,0 failed; одно предупреждение доступа к .pytest_cache.
|
||||
git diff --check PASS. Lab051 git add/commit и push не выполнялись; индекс пуст.
|
||||
RF TX=0; HackRF,180 МГц,QPSK,новый FEC/кодек и аппаратные500 ксимв/с не запускались.
|
||||
|
||||
Следующий этап: STOP. Рекомендуемый Lab052 — отдельное программное исследование
|
||||
стоимости уникальных фоновых кандидатов в полном режиме5 изображений/с,
|
||||
с неизменными PHY/FEC/CRC и независимыми положительными/отрицательными регрессиями.
|
||||
До отдельного разрешения не выполнять 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.
|
||||
|
||||
385
data/processed/lab050/receiver_benchmarks.json
Normal file
385
data/processed/lab050/receiver_benchmarks.json
Normal file
@@ -0,0 +1,385 @@
|
||||
[
|
||||
{
|
||||
"symbol_rate": 200000,
|
||||
"samples_per_symbol": 40,
|
||||
"detection_decimation": 20,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.792483125,
|
||||
"payload_airtime_seconds": 0.69192,
|
||||
"processing_seconds": 35.12416689947713,
|
||||
"processing_per_rf_second": 44.32165908829558,
|
||||
"processing_per_payload_airtime_second": 50.76333521140758,
|
||||
"coarse_seconds": 2.0254860994173214,
|
||||
"strict_seconds": 32.75070630130358,
|
||||
"assembly_jpeg_decode_seconds": 0.0013477999018505216,
|
||||
"processing_seconds_per_rf_packet": 1.4635069541448804,
|
||||
"peak_iq_buffer_samples": 853027,
|
||||
"iq_buffer_limit_samples": 853556,
|
||||
"working_arrays_estimate_bytes": 150378080,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2191,
|
||||
"peak_candidates_in_block": 2145,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2146,
|
||||
"TRUNCATED_IQ_WINDOW": 21
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.106195,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 1854754,
|
||||
"emitted_after_sample": 2382902
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1855061,
|
||||
"last_sample": 3705166,
|
||||
"emitted_after_sample": 4299138
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 3705569,
|
||||
"last_sample": 5555346,
|
||||
"emitted_after_sample": 6145669
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
{
|
||||
"symbol_rate": 400000,
|
||||
"samples_per_symbol": 20,
|
||||
"detection_decimation": 10,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.401868125,
|
||||
"payload_airtime_seconds": 0.34596,
|
||||
"processing_seconds": 59.47086290002335,
|
||||
"processing_per_rf_second": 147.98601630826866,
|
||||
"processing_per_payload_airtime_second": 171.90097959308403,
|
||||
"coarse_seconds": 0.6804420000407845,
|
||||
"strict_seconds": 58.67772239865735,
|
||||
"assembly_jpeg_decode_seconds": 0.0011727997334674,
|
||||
"processing_seconds_per_rf_packet": 2.4779526208343063,
|
||||
"peak_iq_buffer_samples": 493587,
|
||||
"iq_buffer_limit_samples": 496316,
|
||||
"working_arrays_estimate_bytes": 87503840,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2575,
|
||||
"peak_candidates_in_block": 2364,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2516,
|
||||
"TRUNCATED_SYMBOLS": 17,
|
||||
"TRUNCATED_IQ_WINDOW": 18
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.06154,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 932194,
|
||||
"emitted_after_sample": 1229702
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 932501,
|
||||
"last_sample": 1860046,
|
||||
"emitted_after_sample": 2157842
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1860449,
|
||||
"last_sample": 2787666,
|
||||
"emitted_after_sample": 3050309
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
{
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"detection_decimation": 8,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.323744625,
|
||||
"payload_airtime_seconds": 0.276768,
|
||||
"processing_seconds": 55.15755260013975,
|
||||
"processing_per_rf_second": 170.37364743936598,
|
||||
"processing_per_payload_airtime_second": 199.29165438251442,
|
||||
"coarse_seconds": 0.49211360013578087,
|
||||
"strict_seconds": 54.585659996955656,
|
||||
"assembly_jpeg_decode_seconds": 0.001369199831970036,
|
||||
"processing_seconds_per_rf_packet": 2.2982313583391565,
|
||||
"peak_iq_buffer_samples": 424309,
|
||||
"iq_buffer_limit_samples": 424864,
|
||||
"working_arrays_estimate_bytes": 74928288,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2374,
|
||||
"peak_candidates_in_block": 2114,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2298,
|
||||
"TRUNCATED_SYMBOLS": 38,
|
||||
"TRUNCATED_IQ_WINDOW": 14
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.052608,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 747682,
|
||||
"emitted_after_sample": 1025782
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 747989,
|
||||
"last_sample": 1491022,
|
||||
"emitted_after_sample": 1742690
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1491425,
|
||||
"last_sample": 2234130,
|
||||
"emitted_after_sample": 2496773
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
{
|
||||
"symbol_rate": 800000,
|
||||
"samples_per_symbol": 10,
|
||||
"detection_decimation": 5,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.206560625,
|
||||
"payload_airtime_seconds": 0.17298,
|
||||
"processing_seconds": 60.9549745998811,
|
||||
"processing_per_rf_second": 295.09484007361567,
|
||||
"processing_per_payload_airtime_second": 352.38163140178693,
|
||||
"coarse_seconds": 0.2952118997927755,
|
||||
"strict_seconds": 60.60142269870266,
|
||||
"assembly_jpeg_decode_seconds": 0.0011926002334803343,
|
||||
"processing_seconds_per_rf_packet": 2.539790608328379,
|
||||
"peak_iq_buffer_samples": 308819,
|
||||
"iq_buffer_limit_samples": 317696,
|
||||
"working_arrays_estimate_bytes": 56066720,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 3113,
|
||||
"peak_candidates_in_block": 2707,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 3002,
|
||||
"TRUNCATED_SYMBOLS": 87
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.0392125,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 470914,
|
||||
"emitted_after_sample": 645226
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 471221,
|
||||
"last_sample": 937486,
|
||||
"emitted_after_sample": 1112086
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 937889,
|
||||
"last_sample": 1403826,
|
||||
"emitted_after_sample": 1600933
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
{
|
||||
"symbol_rate": 1000000,
|
||||
"samples_per_symbol": 8,
|
||||
"detection_decimation": 4,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.167498625,
|
||||
"payload_airtime_seconds": 0.138384,
|
||||
"processing_seconds": 55.921951099997386,
|
||||
"processing_per_rf_second": 333.86513531079663,
|
||||
"processing_per_payload_airtime_second": 404.1070578968478,
|
||||
"coarse_seconds": 0.20818859990686178,
|
||||
"strict_seconds": 55.66615750128403,
|
||||
"assembly_jpeg_decode_seconds": 0.0011203999165445566,
|
||||
"processing_seconds_per_rf_packet": 2.3300812958332244,
|
||||
"peak_iq_buffer_samples": 279589,
|
||||
"iq_buffer_limit_samples": 281968,
|
||||
"working_arrays_estimate_bytes": 49778592,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2995,
|
||||
"peak_candidates_in_block": 2361,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2832,
|
||||
"TRUNCATED_SYMBOLS": 96,
|
||||
"TRUNCATED_IQ_WINDOW": 43
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.034746,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 378658,
|
||||
"emitted_after_sample": 565220
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 378965,
|
||||
"last_sample": 752974,
|
||||
"emitted_after_sample": 892978
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 753377,
|
||||
"last_sample": 1127058,
|
||||
"emitted_after_sample": 1324165
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
}
|
||||
]
|
||||
209
data/processed/lab051/baseline_profile.json
Normal file
209
data/processed/lab051/baseline_profile.json
Normal file
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"benchmark": {
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"detection_decimation": 8,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.323744625,
|
||||
"payload_airtime_seconds": 0.276768,
|
||||
"processing_seconds": 49.61432680033613,
|
||||
"processing_per_rf_second": 153.25143019852803,
|
||||
"processing_per_payload_airtime_second": 179.26323419013804,
|
||||
"coarse_seconds": 0.4982285003643483,
|
||||
"strict_seconds": 49.04027030314319,
|
||||
"assembly_jpeg_decode_seconds": 0.0013099998468533158,
|
||||
"processing_seconds_per_rf_packet": 2.067263616680672,
|
||||
"peak_iq_buffer_samples": 424309,
|
||||
"iq_buffer_limit_samples": 424864,
|
||||
"working_arrays_estimate_bytes": 74928288,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2374,
|
||||
"peak_candidates_in_block": 2114,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2298,
|
||||
"TRUNCATED_SYMBOLS": 38,
|
||||
"TRUNCATED_IQ_WINDOW": 14
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.052608,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 747682,
|
||||
"emitted_after_sample": 1025782
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 747989,
|
||||
"last_sample": 1491022,
|
||||
"emitted_after_sample": 1742690
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1491425,
|
||||
"last_sample": 2234130,
|
||||
"emitted_after_sample": 2496773
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
"profile": {
|
||||
"shared_stages": {
|
||||
"phy_header": {
|
||||
"calls": 24,
|
||||
"seconds": 0.0005645002238452435,
|
||||
"exclusive_seconds": 0.0005645002238452435
|
||||
},
|
||||
"coarse_localization": {
|
||||
"calls": 36,
|
||||
"seconds": 0.4980395002057776,
|
||||
"exclusive_seconds": 0.4980395002057776
|
||||
}
|
||||
},
|
||||
"valid": {
|
||||
"count": 24,
|
||||
"mean_seconds": 0.07296675831700365,
|
||||
"median_seconds": 0.07252859993604943,
|
||||
"total_seconds": 1.7512021996080875,
|
||||
"stages": {
|
||||
"refinement": {
|
||||
"calls": 24,
|
||||
"seconds": 0.028973900014534593,
|
||||
"exclusive_seconds": 0.028973900014534593
|
||||
},
|
||||
"matched_filter": {
|
||||
"calls": 48,
|
||||
"seconds": 0.1906564999371767,
|
||||
"exclusive_seconds": 0.1906564999371767
|
||||
},
|
||||
"timing": {
|
||||
"calls": 48,
|
||||
"seconds": 0.9146315004909411,
|
||||
"exclusive_seconds": 0.0007350004743784666
|
||||
},
|
||||
"timing_fallback": {
|
||||
"calls": 48,
|
||||
"seconds": 0.9138965000165626,
|
||||
"exclusive_seconds": 0.9138965000165626
|
||||
},
|
||||
"phase_diagnostics": {
|
||||
"calls": 48,
|
||||
"seconds": 0.019832400372251868,
|
||||
"exclusive_seconds": 0.008994300849735737
|
||||
},
|
||||
"cfo_gain": {
|
||||
"calls": 48,
|
||||
"seconds": 0.010838099522516131,
|
||||
"exclusive_seconds": 0.010838099522516131
|
||||
},
|
||||
"phy_header": {
|
||||
"calls": 48,
|
||||
"seconds": 0.0017112999921664596,
|
||||
"exclusive_seconds": 0.0017112999921664596
|
||||
},
|
||||
"body_pipeline": {
|
||||
"calls": 24,
|
||||
"seconds": 0.49711450026370585,
|
||||
"exclusive_seconds": 0.011535500641912222
|
||||
},
|
||||
"viterbi": {
|
||||
"calls": 24,
|
||||
"seconds": 0.4843809995800257,
|
||||
"exclusive_seconds": 0.4843809995800257
|
||||
},
|
||||
"crc_parser": {
|
||||
"calls": 24,
|
||||
"seconds": 0.0002698999596759677,
|
||||
"exclusive_seconds": 0.0002698999596759677
|
||||
}
|
||||
}
|
||||
},
|
||||
"false": {
|
||||
"count": 2350,
|
||||
"mean_seconds": 0.02009875251049612,
|
||||
"median_seconds": 0.019996499991975725,
|
||||
"total_seconds": 47.23206839966588,
|
||||
"stages": {
|
||||
"refinement": {
|
||||
"calls": 2350,
|
||||
"seconds": 2.7974041998386383,
|
||||
"exclusive_seconds": 2.7974041998386383
|
||||
},
|
||||
"matched_filter": {
|
||||
"calls": 2336,
|
||||
"seconds": 0.9339654004434124,
|
||||
"exclusive_seconds": 0.9339654004434124
|
||||
},
|
||||
"timing": {
|
||||
"calls": 2336,
|
||||
"seconds": 41.83316580078099,
|
||||
"exclusive_seconds": 0.028656100272201
|
||||
},
|
||||
"timing_fallback": {
|
||||
"calls": 2336,
|
||||
"seconds": 41.80450970050879,
|
||||
"exclusive_seconds": 41.80450970050879
|
||||
},
|
||||
"phase_diagnostics": {
|
||||
"calls": 2336,
|
||||
"seconds": 0.910528302541934,
|
||||
"exclusive_seconds": 0.4247559007490054
|
||||
},
|
||||
"cfo_gain": {
|
||||
"calls": 2336,
|
||||
"seconds": 0.4857724017929286,
|
||||
"exclusive_seconds": 0.4857724017929286
|
||||
},
|
||||
"phy_header": {
|
||||
"calls": 2298,
|
||||
"seconds": 0.06713570095598698,
|
||||
"exclusive_seconds": 0.06713570095598698
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rf_tx": 0,
|
||||
"array_observation": {
|
||||
"valid": {
|
||||
"observed_array_objects": 14361,
|
||||
"observed_owned_arrays": 8704,
|
||||
"observed_owned_bytes_total": 16681890,
|
||||
"tracemalloc_retained_bytes": 1842,
|
||||
"tracemalloc_peak_bytes": 4787123,
|
||||
"array_count_is_lower_bound": true
|
||||
}
|
||||
},
|
||||
"process_memory_after_profiling": {
|
||||
"rss_bytes": 156041216,
|
||||
"peak_rss_bytes": 169754624
|
||||
}
|
||||
}
|
||||
419
data/processed/lab051/benchmark.json
Normal file
419
data/processed/lab051/benchmark.json
Normal file
@@ -0,0 +1,419 @@
|
||||
{
|
||||
"repeats": [
|
||||
{
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"detection_decimation": 8,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.323744625,
|
||||
"payload_airtime_seconds": 0.276768,
|
||||
"processing_seconds": 1.2077819011174142,
|
||||
"processing_per_rf_second": 3.7306624044103103,
|
||||
"processing_per_payload_airtime_second": 4.363878414836304,
|
||||
"coarse_seconds": 0.0927895000204444,
|
||||
"strict_seconds": 0.9242713990388438,
|
||||
"assembly_jpeg_decode_seconds": 0.0011941003613173962,
|
||||
"processing_seconds_per_rf_packet": 0.05032424587989226,
|
||||
"peak_iq_buffer_samples": 424309,
|
||||
"iq_buffer_limit_samples": 424864,
|
||||
"working_arrays_estimate_bytes": 74928288,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2374,
|
||||
"peak_candidates_in_block": 2114,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2298,
|
||||
"TRUNCATED_SYMBOLS": 38,
|
||||
"TRUNCATED_IQ_WINDOW": 14
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.052608,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 747682,
|
||||
"emitted_after_sample": 1025782
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 747989,
|
||||
"last_sample": 1491022,
|
||||
"emitted_after_sample": 1742690
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1491425,
|
||||
"last_sample": 2234130,
|
||||
"emitted_after_sample": 2496773
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
{
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"detection_decimation": 8,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.323744625,
|
||||
"payload_airtime_seconds": 0.276768,
|
||||
"processing_seconds": 1.144175999914296,
|
||||
"processing_per_rf_second": 3.534193038461398,
|
||||
"processing_per_payload_airtime_second": 4.134061740932101,
|
||||
"coarse_seconds": 0.08686010027304292,
|
||||
"strict_seconds": 0.8724833994638175,
|
||||
"assembly_jpeg_decode_seconds": 0.0011348994448781013,
|
||||
"processing_seconds_per_rf_packet": 0.047673999996428996,
|
||||
"peak_iq_buffer_samples": 424309,
|
||||
"iq_buffer_limit_samples": 424864,
|
||||
"working_arrays_estimate_bytes": 74928288,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2374,
|
||||
"peak_candidates_in_block": 2114,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2298,
|
||||
"TRUNCATED_SYMBOLS": 38,
|
||||
"TRUNCATED_IQ_WINDOW": 14
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.052608,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 747682,
|
||||
"emitted_after_sample": 1025782
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 747989,
|
||||
"last_sample": 1491022,
|
||||
"emitted_after_sample": 1742690
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1491425,
|
||||
"last_sample": 2234130,
|
||||
"emitted_after_sample": 2496773
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
{
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"detection_decimation": 8,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.323744625,
|
||||
"payload_airtime_seconds": 0.276768,
|
||||
"processing_seconds": 1.1161368001485243,
|
||||
"processing_per_rf_second": 3.4475840337071983,
|
||||
"processing_per_payload_airtime_second": 4.032752341847773,
|
||||
"coarse_seconds": 0.08814430015627295,
|
||||
"strict_seconds": 0.841466698795557,
|
||||
"assembly_jpeg_decode_seconds": 0.0011885995045304298,
|
||||
"processing_seconds_per_rf_packet": 0.04650570000618851,
|
||||
"peak_iq_buffer_samples": 424309,
|
||||
"iq_buffer_limit_samples": 424864,
|
||||
"working_arrays_estimate_bytes": 74928288,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2374,
|
||||
"peak_candidates_in_block": 2114,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2298,
|
||||
"TRUNCATED_SYMBOLS": 38,
|
||||
"TRUNCATED_IQ_WINDOW": 14
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.052608,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 747682,
|
||||
"emitted_after_sample": 1025782
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 747989,
|
||||
"last_sample": 1491022,
|
||||
"emitted_after_sample": 1742690
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1491425,
|
||||
"last_sample": 2234130,
|
||||
"emitted_after_sample": 2496773
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
}
|
||||
],
|
||||
"median_processing_seconds": 1.144175999914296,
|
||||
"profile": {
|
||||
"shared_stages": {
|
||||
"phy_header": {
|
||||
"calls": 24,
|
||||
"seconds": 0.00042649987153708935,
|
||||
"exclusive_seconds": 0.00042649987153708935
|
||||
},
|
||||
"coarse_localization": {
|
||||
"calls": 36,
|
||||
"seconds": 0.08918269968125969,
|
||||
"exclusive_seconds": 0.08918269968125969
|
||||
}
|
||||
},
|
||||
"valid": {
|
||||
"count": 24,
|
||||
"mean_seconds": 0.01776749997710188,
|
||||
"median_seconds": 0.017930250032804906,
|
||||
"total_seconds": 0.4264199994504452,
|
||||
"stages": {
|
||||
"refinement": {
|
||||
"calls": 24,
|
||||
"seconds": 0.09387810016050935,
|
||||
"exclusive_seconds": 0.09387810016050935
|
||||
},
|
||||
"matched_filter": {
|
||||
"calls": 48,
|
||||
"seconds": 0.1028753004502505,
|
||||
"exclusive_seconds": 0.1028753004502505
|
||||
},
|
||||
"timing": {
|
||||
"calls": 48,
|
||||
"seconds": 0.04353450005874038,
|
||||
"exclusive_seconds": 0.04353450005874038
|
||||
},
|
||||
"phase_diagnostics": {
|
||||
"calls": 48,
|
||||
"seconds": 0.016765999956987798,
|
||||
"exclusive_seconds": 0.008323899819515646
|
||||
},
|
||||
"cfo_gain": {
|
||||
"calls": 48,
|
||||
"seconds": 0.008442100137472153,
|
||||
"exclusive_seconds": 0.008442100137472153
|
||||
},
|
||||
"phy_header": {
|
||||
"calls": 48,
|
||||
"seconds": 0.001451600226573646,
|
||||
"exclusive_seconds": 0.001451600226573646
|
||||
},
|
||||
"body_pipeline": {
|
||||
"calls": 24,
|
||||
"seconds": 0.14421880000736564,
|
||||
"exclusive_seconds": 0.011199799599125981
|
||||
},
|
||||
"viterbi": {
|
||||
"calls": 24,
|
||||
"seconds": 0.1321190003072843,
|
||||
"exclusive_seconds": 0.1321190003072843
|
||||
},
|
||||
"crc_parser": {
|
||||
"calls": 24,
|
||||
"seconds": 0.00021099986042827368,
|
||||
"exclusive_seconds": 0.00021099986042827368
|
||||
}
|
||||
}
|
||||
},
|
||||
"false": {
|
||||
"count": 2350,
|
||||
"mean_seconds": 0.00018731676512695057,
|
||||
"median_seconds": 6.049999501556158e-05,
|
||||
"total_seconds": 0.4401943980483338,
|
||||
"stages": {
|
||||
"refinement": {
|
||||
"calls": 2350,
|
||||
"seconds": 0.11974260024726391,
|
||||
"exclusive_seconds": 0.11974260024726391
|
||||
},
|
||||
"matched_filter": {
|
||||
"calls": 174,
|
||||
"seconds": 0.026502199470996857,
|
||||
"exclusive_seconds": 0.026502199470996857
|
||||
},
|
||||
"timing": {
|
||||
"calls": 174,
|
||||
"seconds": 0.13858849939424545,
|
||||
"exclusive_seconds": 0.13858849939424545
|
||||
},
|
||||
"phase_diagnostics": {
|
||||
"calls": 174,
|
||||
"seconds": 0.05482260056305677,
|
||||
"exclusive_seconds": 0.028438000474125147
|
||||
},
|
||||
"cfo_gain": {
|
||||
"calls": 174,
|
||||
"seconds": 0.02638460008893162,
|
||||
"exclusive_seconds": 0.02638460008893162
|
||||
},
|
||||
"phy_header": {
|
||||
"calls": 2298,
|
||||
"seconds": 0.03204370115417987,
|
||||
"exclusive_seconds": 0.03204370115417987
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"profiled_benchmark": {
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"detection_decimation": 8,
|
||||
"input_images": 3,
|
||||
"tx_rf_packets": 24,
|
||||
"accepted_rf_packets": 24,
|
||||
"completed_images": 3,
|
||||
"all_sha_match": true,
|
||||
"packets_before_finish": 24,
|
||||
"images_before_finish": 3,
|
||||
"capture_seconds": 0.323744625,
|
||||
"payload_airtime_seconds": 0.276768,
|
||||
"processing_seconds": 1.16440529958345,
|
||||
"processing_per_rf_second": 3.5966783991655458,
|
||||
"processing_per_payload_airtime_second": 4.207152920798105,
|
||||
"coarse_seconds": 0.08932610007468611,
|
||||
"strict_seconds": 0.8848743984708562,
|
||||
"assembly_jpeg_decode_seconds": 0.0011538001708686352,
|
||||
"processing_seconds_per_rf_packet": 0.048516887482643746,
|
||||
"peak_iq_buffer_samples": 424309,
|
||||
"iq_buffer_limit_samples": 424864,
|
||||
"working_arrays_estimate_bytes": 74928288,
|
||||
"memory_is_process_rss_measurement": false,
|
||||
"peak_object_payload_bytes": 1821,
|
||||
"candidate_count": 2374,
|
||||
"peak_candidates_in_block": 2114,
|
||||
"rejected_reasons": {
|
||||
"PHY_HEADER_MAGIC_MISMATCH": 2298,
|
||||
"TRUNCATED_SYMBOLS": 38,
|
||||
"TRUNCATED_IQ_WINDOW": 14
|
||||
},
|
||||
"buffer_release_delay_upper_seconds": 0.052608,
|
||||
"noisy": true,
|
||||
"coded_bit_ebn0_db": 7,
|
||||
"cfo_hz": 10,
|
||||
"candidate_threshold": 0.030964473262429237,
|
||||
"completed": [
|
||||
{
|
||||
"image_id": 1,
|
||||
"sha256": "15723773a8788c9d610c874707ad2d513582322ee1d42009b0b39d3de336e10d",
|
||||
"bytes": 1740,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 4321,
|
||||
"last_sample": 747682,
|
||||
"emitted_after_sample": 1025782
|
||||
},
|
||||
{
|
||||
"image_id": 2,
|
||||
"sha256": "66404b14ca9e3a6627871ad9d3b5dd4311915d7820a99dfa182fc3a20fbe9d85",
|
||||
"bytes": 1790,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 747989,
|
||||
"last_sample": 1491022,
|
||||
"emitted_after_sample": 1742690
|
||||
},
|
||||
{
|
||||
"image_id": 3,
|
||||
"sha256": "166d33d72820bf3385e0d30823d991b453147c84ca75f788ab8819ad5e3c3ced",
|
||||
"bytes": 1821,
|
||||
"dimensions": [
|
||||
128,
|
||||
96
|
||||
],
|
||||
"first_sample": 1491425,
|
||||
"last_sample": 2234130,
|
||||
"emitted_after_sample": 2496773
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
},
|
||||
"rf_tx": 0
|
||||
}
|
||||
67
data/processed/lab051/memory.json
Normal file
67
data/processed/lab051/memory.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"blocks": 4096,
|
||||
"model_seconds": 33.554432,
|
||||
"background": "zeros; noisy background covered by six-second integration",
|
||||
"peak_iq_bytes": 3177728,
|
||||
"iq_limit_bytes": 3398912,
|
||||
"retained_after_finish": 0,
|
||||
"checkpoints": [
|
||||
{
|
||||
"block": 512,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163840000,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 1024,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163840000,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 1536,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163786752,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 2048,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163786752,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 2560,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163786752,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 3072,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163786752,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 3584,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163786752,
|
||||
"peak_rss_bytes": 186650624
|
||||
},
|
||||
{
|
||||
"block": 4096,
|
||||
"iq_bytes": 2653440,
|
||||
"decimated_bytes": 397216,
|
||||
"rss_bytes": 163790848,
|
||||
"peak_rss_bytes": 186650624
|
||||
}
|
||||
],
|
||||
"rf_tx": 0
|
||||
}
|
||||
198
data/processed/lab051/regression20k/result.json
Normal file
198
data/processed/lab051/regression20k/result.json
Normal 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": 12.978691199910827,
|
||||
"coarse_search_seconds": 4.4797875001095235,
|
||||
"strict_stage_seconds": 8.487128499895334,
|
||||
"average_strict_candidate_seconds": 0.12300186231732368,
|
||||
"approximate_peak_bytes": 1545755352,
|
||||
"stage_measurements": {
|
||||
"cs8_to_complex64": {
|
||||
"calls": 113,
|
||||
"cumulative_seconds": 1.306003100471571
|
||||
},
|
||||
"refine_start": {
|
||||
"calls": 69,
|
||||
"cumulative_seconds": 2.930933100869879
|
||||
},
|
||||
"matched_filter": {
|
||||
"calls": 105,
|
||||
"cumulative_seconds": 2.9355400999775156
|
||||
},
|
||||
"timing": {
|
||||
"calls": 105,
|
||||
"cumulative_seconds": 1.2902987005654722
|
||||
},
|
||||
"cfo_phase_soft": {
|
||||
"calls": 105,
|
||||
"cumulative_seconds": 0.039461099775508046
|
||||
},
|
||||
"phy_header": {
|
||||
"calls": 105,
|
||||
"cumulative_seconds": 0.00410620030015707
|
||||
},
|
||||
"post_preamble_fec_crc": {
|
||||
"calls": 36,
|
||||
"cumulative_seconds": 0.6347944003064185
|
||||
},
|
||||
"viterbi": {
|
||||
"calls": 36,
|
||||
"cumulative_seconds": 0.6168836002470925
|
||||
}
|
||||
},
|
||||
"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.883488745018403,
|
||||
"processing_to_capture_ratio": 0.8652460799940551,
|
||||
"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
|
||||
}
|
||||
40
data/processed/lab051/replay.json
Normal file
40
data/processed/lab051/replay.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"false": {
|
||||
"reason": "PHY_HEADER_MAGIC_MISMATCH",
|
||||
"baseline_arrays": {
|
||||
"observed_array_objects": 2888,
|
||||
"observed_owned_arrays": 61,
|
||||
"observed_owned_bytes_total": 6612268,
|
||||
"tracemalloc_retained_bytes": 1481,
|
||||
"tracemalloc_peak_bytes": 2104129,
|
||||
"array_count_is_lower_bound": true
|
||||
},
|
||||
"optimized_arrays": {
|
||||
"observed_array_objects": 76,
|
||||
"observed_owned_arrays": 61,
|
||||
"observed_owned_bytes_total": 5906101,
|
||||
"tracemalloc_retained_bytes": 1936,
|
||||
"tracemalloc_peak_bytes": 5524392,
|
||||
"array_count_is_lower_bound": true
|
||||
}
|
||||
},
|
||||
"valid": {
|
||||
"reason": "CRC_PASS",
|
||||
"baseline_arrays": {
|
||||
"observed_array_objects": 14361,
|
||||
"observed_owned_arrays": 8704,
|
||||
"observed_owned_bytes_total": 16681890,
|
||||
"tracemalloc_retained_bytes": 2130,
|
||||
"tracemalloc_peak_bytes": 5885299,
|
||||
"array_count_is_lower_bound": true
|
||||
},
|
||||
"optimized_arrays": {
|
||||
"observed_array_objects": 6594,
|
||||
"observed_owned_arrays": 6560,
|
||||
"observed_owned_bytes_total": 12345244,
|
||||
"tracemalloc_retained_bytes": 2645,
|
||||
"tracemalloc_peak_bytes": 7771675,
|
||||
"array_count_is_lower_bound": true
|
||||
}
|
||||
}
|
||||
}
|
||||
7253
data/processed/lab051/stream.json
Normal file
7253
data/processed/lab051/stream.json
Normal file
File diff suppressed because it is too large
Load Diff
27
data/processed/lab052/acceptance.json
Normal file
27
data/processed/lab052/acceptance.json
Normal 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"
|
||||
}
|
||||
13
data/processed/lab052/allocations.json
Normal file
13
data/processed/lab052/allocations.json
Normal 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
|
||||
}
|
||||
24914
data/processed/lab052/baseline_profile.json
Normal file
24914
data/processed/lab052/baseline_profile.json
Normal file
File diff suppressed because it is too large
Load Diff
7257
data/processed/lab052/baseline_stream.json
Normal file
7257
data/processed/lab052/baseline_stream.json
Normal file
File diff suppressed because it is too large
Load Diff
7275
data/processed/lab052/cpu_profile.json
Normal file
7275
data/processed/lab052/cpu_profile.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
data/processed/lab052/detector_distributions.png
Normal file
BIN
data/processed/lab052/detector_distributions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
5303
data/processed/lab052/detector_distributions.svg
Normal file
5303
data/processed/lab052/detector_distributions.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 239 KiB |
74
data/processed/lab052/initial_receiver_cpu.txt
Normal file
74
data/processed/lab052/initial_receiver_cpu.txt
Normal 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)
|
||||
115
data/processed/lab052/memory.json
Normal file
115
data/processed/lab052/memory.json
Normal 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
|
||||
}
|
||||
74
data/processed/lab052/receiver_cpu.txt
Normal file
74
data/processed/lab052/receiver_cpu.txt
Normal 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)
|
||||
198
data/processed/lab052/regression20k/result.json
Normal file
198
data/processed/lab052/regression20k/result.json
Normal 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
|
||||
}
|
||||
124
data/processed/lab052/rejected_nms_comparison.json
Normal file
124
data/processed/lab052/rejected_nms_comparison.json
Normal 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
|
||||
}
|
||||
121
data/processed/lab052/rule_comparison.json
Normal file
121
data/processed/lab052/rule_comparison.json
Normal 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
|
||||
}
|
||||
905
data/processed/lab052/sensitivity.json
Normal file
905
data/processed/lab052/sensitivity.json
Normal 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
|
||||
}
|
||||
186
data/processed/lab052/source_fixture.json
Normal file
186
data/processed/lab052/source_fixture.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
107
data/processed/lab052/statistics.json
Normal file
107
data/processed/lab052/statistics.json
Normal 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
|
||||
}
|
||||
7276
data/processed/lab052/stream.json
Normal file
7276
data/processed/lab052/stream.json
Normal file
File diff suppressed because it is too large
Load Diff
7275
data/processed/lab052/stream_repeat_1.json
Normal file
7275
data/processed/lab052/stream_repeat_1.json
Normal file
File diff suppressed because it is too large
Load Diff
7275
data/processed/lab052/stream_repeat_2.json
Normal file
7275
data/processed/lab052/stream_repeat_2.json
Normal file
File diff suppressed because it is too large
Load Diff
7275
data/processed/lab052/stream_repeat_3.json
Normal file
7275
data/processed/lab052/stream_repeat_3.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -240,6 +240,114 @@
|
||||
- **Далее:** программный Lab050 — профиль RX, поток видео и ограничение возраста очереди.
|
||||
- **RF:** RF TX = 0, STOP.
|
||||
|
||||
## Lab050
|
||||
|
||||
- **Цель:** последовательность JPEG через отдельный SDR-транспорт, ограничение
|
||||
возраста изображения и блочный независимый IQ-приём. Управление ELRS отдельно.
|
||||
- **База:** Lab049 закреплена коммитом
|
||||
`cdf5396e807b83f82a264632dc46f71428fcd616`; Lab050 не добавлена в индекс и не коммитилась.
|
||||
- **Переиспользовано:** планировщик Lab035 с LATEST_ONLY и завершением активного
|
||||
изображения. Добавлены готовность JPEG отдельно от съёмки, произвольный предел
|
||||
возраста и прогноз RX. Поздно готовый старый снимок не вытесняет свежий.
|
||||
- **Профили:** A 320×240 RGB q35 — 8724 байта; B 160×120 RGB q35 — 2771;
|
||||
C 128×96 RGB q25 — 1740; D 160×120 grayscale q30 — 2028. По 30
|
||||
детерминированных сдвигов исходного фото; это не видео камеры.
|
||||
- **Матрица:** 240 сценариев, вход 3/5/10 кадров/с, Rs=200/400/500/800/1000 ксимв/с,
|
||||
MAX_VIDEO_AGE=200/333/500/1000 мс. Ожидающая очередь ≤1.
|
||||
- **Идеальная модель:** C/500k — 5 или 10 свежих кадров/с, возраст около 92,6 мс.
|
||||
При камере 10 и Rs=200k, возрасте 333 мс: 14 передач, 16 замен, 4,333 свежих
|
||||
кадра/с внутри окна и один дополнительный после него.
|
||||
- **Потоковый RX:** 24/24 RF-пакета и 3/3 точных JPEG до `finish` на каждой скорости;
|
||||
IQ-память ограничена. В шумном опыте processing/RF = 44,322 / 147,986 /
|
||||
170,374 / 295,095 / 333,865. Реального времени нет.
|
||||
- **Память:** удерживаемые IQ 2,13–6,51 МиБ; расчёт массивов 47,5–143,4 МиБ,
|
||||
не измеренный RSS. Состояние RX не растёт вместе с длиной сессии.
|
||||
- **Достижимость:** 5/10 кадров/с без QPSK — CONDITIONAL. C≤1952 байт,
|
||||
500 ксимв/с, стоимость RX-пакета ≤16/12 мс для модели возраста 200 мс;
|
||||
требуется ещё примерно 144/192-кратное ускорение относительно этого замера.
|
||||
- **Код:** [видеомодель](../experiments/lab050_video_stream.py),
|
||||
[потоковый RX](../experiments/lab050_streaming_receiver.py).
|
||||
- **Проверки:** [44 теста Lab050](../tests/test_lab050_video_stream.py),
|
||||
quick_gate — 650 тестов и 14 функциональных проверок; независимые verifier/adversary.
|
||||
- **Отчёт:** [состояние](lab050_current_state.md),
|
||||
[JSON](../data/processed/lab050/result.json),
|
||||
[замеры RX](../data/processed/lab050/receiver_benchmarks.json). Журнал: запись 039.
|
||||
- **Далее:** Lab051 — профиль и ускорение строгой обработки ложных кандидатов.
|
||||
- **RF:** RF TX = 0. Lab050 commit/push не выполнялись. STOP.
|
||||
|
||||
## Lab051
|
||||
|
||||
- **Цель:** алгоритмически ускорить независимый блочный RX на 500 ксимв/с,
|
||||
16 отсчётов/символ, Fs=8 Мвыб/с; проверить 5 свежих JPEG C в секунду.
|
||||
- **База:** Lab050 закреплена разрешённым коммитом
|
||||
`60691a09f193f36983c7174c5252ed0897c559e9`.
|
||||
- **Профиль:** главная стоимость Lab050 — fallback timing, 41,833 с на ложных
|
||||
кандидатах. Полный диапазон 1601 смещения сохранён и вычисляется через FFT
|
||||
и накопленные энергии, с исходным выбором на неразделимых пиках.
|
||||
- **Изменения:** общий front-end, точное повторное использование децимации,
|
||||
кэш результатов уточнённого окна, короткий probe до PHY PASS, предвычисленные
|
||||
метрики прежнего Viterbi. Порог, PHY/FEC/CRC и protocol не менялись.
|
||||
- **Короткий контроль:** 24/24 пакета, 3/3 точных JPEG; медиана трёх повторов
|
||||
1,144176 с вместо исторических 55,157553 с. Processing/RF=3,534193.
|
||||
- **Основной поток:** 30 JPEG за 6 с, 240/240 пакетов, SHA MATCH 30/30.
|
||||
Обработка 6,044416 с IQ заняла 38,630132 с, processing/RF=6,391045.
|
||||
При MAX_VIDEO_AGE=200 мс все 30 JPEG просрочены; FRESH DISPLAY FPS=0.
|
||||
Цель реального времени не достигнута, включая минимальную свежесть >0.
|
||||
- **Память:** пик удерживаемого IQ 3,031 МиБ, RSS процесса в опыте измерен.
|
||||
Тест 33,55 с не показывает роста IQ/кэша с длительностью. Это не готовая live-
|
||||
очередь: непрерывный источник потребовал бы около 2,09 ГБ внешнего backlog.
|
||||
- **Регрессия:** сохранённая Lab047-запись 20 ксимв/с штатным Lab048:
|
||||
36/36, CRC PASS, JPEG SHA MATCH; 19 ошибок до FEC и 0 после.
|
||||
- **Код:** [приёмник](../experiments/lab051_streaming_receiver.py),
|
||||
[профилировщик](../experiments/lab051_receiver_profile.py),
|
||||
[полный поток](../experiments/lab051_receiver_performance.py),
|
||||
[38 тестов](../tests/test_lab051_streaming_receiver.py).
|
||||
- **Отчёт:** [состояние](lab051_current_state.md),
|
||||
[измеренный поток](../data/processed/lab051/stream.json). Журнал: запись 040.
|
||||
- **Далее:** отдельное программное исследование стоимости уникальных фоновых
|
||||
кандидатов в полном потоке 5 кадров/с с тем же PHY/FEC/CRC.
|
||||
- **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`, бинарные объекты и другие аппаратные исходные
|
||||
|
||||
379
docs/lab050_current_state.md
Normal file
379
docs/lab050_current_state.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# Lab050: потоковая архитектура видео и возраст изображения
|
||||
|
||||
8 сентября 2026 года. SOFTWARE ONLY. RF TX = 0. Оборудование не запускалось
|
||||
и не инвентаризировалось. Управление ELRS/CRSF и SDR-видео остаются раздельными.
|
||||
|
||||
**Получен блочный независимый RX с ограниченным IQ-буфером и модель свежей
|
||||
видеопередачи. Текущий RX не обеспечивает 5 или 10 свежих изображений/с.**
|
||||
По времени радио оба уровня возможны на уменьшенном JPEG без QPSK; итог
|
||||
полной системы — CONDITIONAL, с конкретными ещё не выполненными условиями ниже.
|
||||
|
||||
## Фиксация Lab049 и исходный аудит
|
||||
|
||||
Исходный HEAD `ac6cc792b896e34f7dd6d291955a0387fdafcf41`, ветка main.
|
||||
Проверены status, diff/stat, diff/check и точный staged scope. В отдельный
|
||||
коммит вошли 12 файлов Lab049: три скрипта, три файла тестов, три отчёта,
|
||||
README, PROJECT_LOG и LABS_INDEX. Сгенерированные данные, `.codex`, локальные
|
||||
AGENTS/CLAUDE и посторонние файлы не добавлены.
|
||||
|
||||
Создан разрешённый коммит:
|
||||
`cdf5396e807b83f82a264632dc46f71428fcd616`
|
||||
— `Lab049: define ELRS control and SDR video architecture`.
|
||||
От него выполнена Lab050; её изменения не добавлялись в индекс и не коммитились.
|
||||
Push/pull/fetch не выполнялись.
|
||||
|
||||
Аудит Lab034–037 и связанных модулей показал:
|
||||
|
||||
| Уже существовало | Как используется в Lab050 |
|
||||
|---|---|
|
||||
| `image_fragments.py`, Lab046: image ID, индекс/число фрагментов, размер объекта | Неизменная фрагментация JPEG, отдельный ID каждого снимка |
|
||||
| Lab034: группировка изображения и изоляция стирающего FEC | Переиспользована семантика границ изображения; старый 12+3 FEC не включён |
|
||||
| Lab035, `video_frame_scheduler.py`: активный кадр, очередь ожидающих, LATEST_ONLY, прогноз завершения | Расширен существующий планировщик; новая независимая очередь не создана |
|
||||
| Lab036–037: один самый свежий ожидающий кадр, завершение активного | Сохранена политика; очередь ожидающих не больше одного изображения |
|
||||
| Lab048: корреляционный поиск и строгий PHY/FEC/CRC | Использованы в блочном API; чтение IQ отделено от memmap |
|
||||
|
||||
Изменения кода: `protocol/video_frame_scheduler.py` получает готовность JPEG
|
||||
отдельно от времени съёмки, произвольный MAX_VIDEO_AGE и прогноз задержки после
|
||||
радио. Старые вызовы и все пять прежних политик сохраняют поведение.
|
||||
`lab046c_offline_recovery.decode_candidate_from_memmap` принимает необязательный
|
||||
читатель IQ; прежнее чтение CS8 остаётся поведением по умолчанию.
|
||||
Новые файлы: [модель и эксперимент](../experiments/lab050_video_stream.py),
|
||||
[блочный RX](../experiments/lab050_streaming_receiver.py),
|
||||
[тесты](../tests/test_lab050_video_stream.py).
|
||||
|
||||
## Политика свежести
|
||||
|
||||
Активное изображение является выбранной атомарной единицей обслуживания:
|
||||
после допуска оно завершается целиком. Отдельный физический пакет тем более
|
||||
не прерывается. Во время передачи каждого пакета принимаются события готовности
|
||||
новых JPEG и заменяется ожидающий снимок. При постоянной скорости, отсутствии
|
||||
CONTROL в SDR и известном числе пакетов точный прогноз допуска позволяет
|
||||
не начинать изображение, уже не укладывающееся в возраст.
|
||||
|
||||
Свежесть определяется парой `(capture_timestamp, image_id)`, а не порядком
|
||||
завершения кодирования. Поздно готовый старый снимок не вытесняет более свежий
|
||||
и не возвращается на экран после опустошения очереди. Обнаруженный независимыми
|
||||
проверяющими отказ этого правила исправлен и покрыт двумя регрессиями.
|
||||
|
||||
Причины удаления различаются: `replaced_by_newest`, `older_capture_arrived_late`,
|
||||
`prediction_reject`; при оценке загруженного RX возможен также отказ допуска
|
||||
к обработке. Если уже нижняя оценка задержки RX делает кадр просроченным,
|
||||
его отклоняет TX-допуск, до занятия эфира. Удаление снимков — нормальная
|
||||
политика видео, а не потеря обязательной управляющей команды.
|
||||
|
||||
Планировщик здесь остаётся событийной моделью с известными входами и историей
|
||||
для отчёта. Его массив входов/результатов зависит от числа исследуемых кадров;
|
||||
**логическая ожидающая очередь** ограничена одним снимком. Готового адаптера
|
||||
живой камеры с ограниченным журналом пока нет. Независимость памяти от длины
|
||||
сессии реализована и проверена непосредственно для IQ-приёмника.
|
||||
|
||||
## Профили JPEG и источник
|
||||
|
||||
Источник — реальное фото `data/raw/lab011_scene.jpg`. Для каждого профиля
|
||||
созданы 30 исходных кадров: первый повторяет исходную подготовку, остальные
|
||||
получены детерминированным циклическим сдвигом `(7i mod 61, 3i mod 31)` пикселей
|
||||
до уменьшения. Это искусственная последовательность одного фото, не съёмка камеры.
|
||||
Разрешение, цвет и quality фиксированы; размеры файлов не подбирались вручную.
|
||||
Во всех профилях subsampling=2, optimize=False, progressive=False.
|
||||
|
||||
| Профиль | Разрешение | Цвет | Quality | Первый JPEG, байт | Диапазон 30 JPEG, байт | PSNR первого, dB |
|
||||
|---|---|---|---:|---:|---:|---:|
|
||||
| [A](../data/processed/lab050/profile_A.jpg) | 320×240 | RGB | 35 | 8724 | 8724–9250 | 28,12 |
|
||||
| [B](../data/processed/lab050/profile_B.jpg) | 160×120 | RGB | 35 | 2771 | 2771–3031 | 28,11 |
|
||||
| [C](../data/processed/lab050/profile_C.jpg) | 128×96 | RGB | 25 | 1740 | 1740–1893 | 27,17 |
|
||||
| [D](../data/processed/lab050/profile_D.jpg) | 160×120 | grayscale | 30 | 2028 | 2028–2233 | 30,15 |
|
||||
|
||||
PSNR = 10 log10(255²/MSE); сравнение с несжатым изображением **того же
|
||||
разрешения и цветового режима** после преобразования. Оно характеризует
|
||||
JPEG-искажение внутри профиля, не потерю деталей при уменьшении и не
|
||||
пригодность картинки для управления. Между RGB и grayscale эти числа не
|
||||
доказывают превосходство качества. Четыре выходных JPEG визуально проверены;
|
||||
субъективный вердикт пригодности для оператора не присваивался.
|
||||
|
||||
Первый A побайтно совпал с Lab047, SHA-256:
|
||||
`e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a`.
|
||||
Исходный контрольный JPEG не изменён. Перекодирование новых профилей разрешено
|
||||
постановкой Lab050; новый видеокодек не разрабатывался.
|
||||
|
||||
## Точная ёмкость нынешнего PHY
|
||||
|
||||
Один RF-пакет переносит 244 байта JPEG плюс 12 байт метаданных фрагмента,
|
||||
дополняется до 256 байт нагрузки packet и до 268 байт вместе с packet header/CRC.
|
||||
FEC R=1/2 K=7, шесть хвостовых бит, перемежитель, PHY header, PRBS7 и BPSK
|
||||
сохранены. RF-пакет занимает 5724 передаваемых бита и 42 дополнительных
|
||||
символьных интервала, итого **5766**. Проверено по настоящему плану Lab045.
|
||||
|
||||
Для JPEG размера J: N=ceil(J/244), T_RF=N×5766/Rs.
|
||||
Точный максимальный размер для f кадров/с:
|
||||
`Jmax = 244 × floor(Rs/(5766×f))`.
|
||||
Это ступенчатая граница с добивкой, не деление полезного битрейта на восемь.
|
||||
|
||||
| Rs, ксимв/с | Выборок/символ при 8 Мвыб/с | Jmax для 3 кадров/с | Jmax для 5 кадров/с | Jmax для 10 кадров/с |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 200 | 40 | 2684 | 1464 | 732 |
|
||||
| 400 | 20 | 5612 | 3172 | 1464 |
|
||||
| 500 | 16 | 6832 | 4148 | 1952 |
|
||||
| 800 | 10 | 11224 | 6588 | 3172 |
|
||||
| 1000 | 8 | 13908 | 8296 | 4148 |
|
||||
|
||||
Все размеры в байтах. На пределе нет запаса для потерь, пауз, объёмных данных
|
||||
или роста JPEG. Никакие ELRS CONTROL/TELEMETRY из этого бюджета не вычитаются.
|
||||
Физическая стоимость старого LinkPacket используется лишь как эквивалентная
|
||||
шкала времени существующего симулятора: его длительность принудительно равна
|
||||
5766/Rs. LinkPacket не добавлен в реальную PHY-упаковку Lab046.
|
||||
|
||||
Максимум изображений/с по времени радио для **первого** JPEG каждого профиля:
|
||||
|
||||
| Rs, ксимв/с | A, 36 пакетов | B, 12 пакетов | C, 8 пакетов | D, 9 пакетов |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 200 | 0,964 | 2,891 | 4,336 | 3,854 |
|
||||
| 400 | 1,927 | 5,781 | 8,672 | 7,708 |
|
||||
| 500 | 2,409 | 7,226 | 10,839 | 9,635 |
|
||||
| 800 | 3,854 | 11,562 | 17,343 | 15,416 |
|
||||
| 1000 | 4,818 | 14,453 | 21,679 | 19,270 |
|
||||
|
||||
По всей последовательности A использует до 38 пакетов, B до 13, D до 10;
|
||||
C остаётся в восьми пакетах. Именно C удобен для следующего воспроизводимого
|
||||
опыта. Профиль B также укладывается по радио в 5 кадров/с на 400 ксимв/с.
|
||||
JPEG A не достигает 5 кадров/с даже на 1 Мсимв/с: 8724 > 8296 байт.
|
||||
|
||||
## Матрица очередей и фактический смысл FPS
|
||||
|
||||
Выполнено 240 сценариев: четыре профиля × входные 3/5/10 кадров/с × пять Rs
|
||||
× пределы возраста 200/333/500/1000 мс. По 30 входных изображений в каждом.
|
||||
Идеальный пакетный путь сериализует packet, проверяет CRC и собирает точные
|
||||
JPEG. Стоимость FEC/поиска IQ в этой матрице сначала равна нулю **по модели**;
|
||||
её реальный замер выполнен отдельно, ниже. Ошибок радио в матрице нет.
|
||||
|
||||
Окно наблюдения H=30/input_fps: 10, 6 или 3 с. Основные RAW RECEIVED FPS и
|
||||
FRESH DISPLAY FPS считают только события внутри H. Полученные при дальнейшем
|
||||
опустошении кадры показываются отдельно. Свежим считается только показанный
|
||||
снимок с возрастом ≤MAX_VIDEO_AGE. Просроченное изображение на экран не выпускается,
|
||||
поэтому raw display FPS совпадает с fresh display FPS; повторный показ старого
|
||||
изображения не засчитывается новым обновлением.
|
||||
|
||||
| Модель | Возраст | Вход | Передано | Удалено до TX | Получено / показано всего | Fresh FPS в H | После H |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| C, камера 5, Rs=500k | 200 мс | 30 | 30 | 0 | 30 / 30 | **5,000** | 0 |
|
||||
| C, камера 10, Rs=500k | 200 мс | 30 | 30 | 0 | 30 / 30 | **10,000** | 0 |
|
||||
| B, камера 5, Rs=400k | 200 мс | 30 | 30 | 0 | 30 / 30 | **5,000** | 0 |
|
||||
| C, камера 10, Rs=200k | 200 мс | 30 | 0 | 30, прогноз возраста | 0 / 0 | **0** | 0 |
|
||||
| C, камера 10, Rs=200k | 333 мс | 30 | 14 | 16, замена свежим | 14 / 14 | **4,333** | 1 |
|
||||
| A, камера 5, Rs=1000k | 200 мс | 30 | 0 | 30, прогноз возраста | 0 / 0 | **0** | 0 |
|
||||
|
||||
Для перегруженного C/10/200k при 333 мс возраст показанных снимков примерно
|
||||
231–329 мс, ожидающая очередь ≤1. Увеличение предела до 500/1000 мс в данном
|
||||
сценарии не меняет 14 передач и 16 замен; оно не создаёт дополнительную ёмкость.
|
||||
При 200 мс уже само радио требует 230,64 мс и все снимки обоснованно отклонены.
|
||||
|
||||
Результат C/5/500k: IMAGE AGE min/median/max = **92,563 / 92,590 / 92,629 мс**.
|
||||
Это возраст **в модели идеального RX и мгновенного экрана**, не камера→монитор.
|
||||
Вход 30, transmitted=received=displayed=30, dropped=0, H=6 с,
|
||||
RAW RECEIVED FPS=RAW DISPLAY FPS=FRESH DISPLAY FPS=5.
|
||||
|
||||
## Блочный приёмник
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
I[IQ-блок до 65536 выборок] --> B[Ограниченный буфер и хвост]
|
||||
B --> C[Грубый поиск в принадлежащем блоку интервале]
|
||||
C --> S[Штатная строгая проверка PHY/FEC/CRC]
|
||||
S --> P[Выдача принятого пакета из push_iq_block]
|
||||
P --> A[Сборщик текущего image ID]
|
||||
A --> J[JPEG decode; далее проверка возраста и экран]
|
||||
```
|
||||
|
||||
`StreamingReceiver.push_iq_block()` принимает непрерывные complex64-блоки
|
||||
без границ TX, payload, ожидаемых ID или числа кадров. `finish()` проверяет
|
||||
остаток, не дополняет недостающую запись нулями. Выдача пакетов не ждёт конца
|
||||
сессии. Глобальные индексы сохраняются; принятая область исключается из
|
||||
повторной строгой проверки. Список кандидатов относится лишь к одному блоку.
|
||||
Внутри RX нет истории принятых пакетов; вызывающий код должен сразу их забирать.
|
||||
|
||||
Размер удерживаемого хвоста определяется максимально допустимым PHY packet
|
||||
1024 байта, а не известными передатчику границами. Сохраняются левый запас
|
||||
уточнения, опорная последовательность, счётчики и текущие IQ. Порог Lab047
|
||||
`0.030964473262429237` не меняется и не подстраивается по успеху JPEG.
|
||||
Скалярная статистика корреляций не объявляется новой калибровкой фона.
|
||||
|
||||
Децимация 20 старого поиска непригодна как универсальная константа для
|
||||
высоких скоростей. В адаптере она равна min(20, samples_per_symbol//2):
|
||||
**20/10/8/5/4** для 200/400/500/800/1000 ксимв/с, оставляя две выборки/символ.
|
||||
Меняются параметры поиска, но не FEC, CRC, модуляция или порог.
|
||||
Константы старых лабораторий восстанавливаются после вызова; это
|
||||
последовательный программный адаптер, не потокобезопасная многопоточная библиотека.
|
||||
|
||||
IQ-блоки должны быть непрерывными. При будущих потерях блоков драйвера нужно
|
||||
явно сбросить незавершённый поиск/объект и обозначить разрыв; live-подключение,
|
||||
обработка таких разрывов и управление переполнением драйвера здесь не реализованы.
|
||||
Синхронный `push` создаёт обратное давление программному источнику; при текущей
|
||||
скорости RX настоящий источник 8 Мвыб/с потребовал бы потерь/переполнения.
|
||||
|
||||
## Интеграционный опыт IQ и производительность
|
||||
|
||||
На **каждой** из пяти скоростей поданы три JPEG C, 24 физических пакета,
|
||||
с неизвестным началом, межкадровыми интервалами и границами входных блоков.
|
||||
CS8-квантование передатчика сохранено. Шум один для всех: coded-bit Eb/N0=7 dB,
|
||||
seed=50050, остаточная частота +10 Гц, фаза 0,73 рад и чередование полярности.
|
||||
После добавления шума поток complex64 не переквантован. Фон также генерируется
|
||||
детерминированно. TX создаёт лишь очередной RF-пакет, целая IQ-запись не хранится.
|
||||
|
||||
**Все пять проходов: 24/24 пакета, 3/3 JPEG, точный SHA; все JPEG выданы до finish.**
|
||||
Сборщик принимает ID/индексы только из проверенных CRC пакетов, сопоставление
|
||||
с TX-хешами происходит после сборки. В этом опыте ID монотонны в одной сессии;
|
||||
wrap/restart видеосеанса не реализован. Разрешён только текущий объект,
|
||||
ограничены его размер (64 КиБ) и число фрагментов; журнал результатов опыта
|
||||
отделён от памяти RX. Дополнительно пройдены интеграционные тесты идеального
|
||||
канала на всех скоростях, границ двух блоков, усечённого потока и длинного фона.
|
||||
|
||||
| Rs, ксимв/с | IQ, с | Обработка, с | Обработка / RF-секунда | Кандидатов строгой проверки | Оценка массивов RX, МиБ |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 200 | 0,792483 | 35,124167 | **44,322** | 2191 | 143,412 |
|
||||
| 400 | 0,401868 | 59,470863 | **147,986** | 2575 | 83,450 |
|
||||
| 500 | 0,323745 | 55,157553 | **170,374** | 2374 | 71,457 |
|
||||
| 800 | 0,206561 | 60,954975 | **295,095** | 3113 | 53,469 |
|
||||
| 1000 | 0,167499 | 55,921951 | **333,865** | 2995 | 47,473 |
|
||||
|
||||
Знаменатель — все поданные IQ, включая фон/хвост. В JSON отдельно есть время
|
||||
чистой передачи полезных RF-пакетов и соответствующее ещё худшее отношение.
|
||||
CPU-замер включает push/finish, сборку и JPEG decode; генерация TX/IQ,
|
||||
преобразование исходной фотографии и сравнение TX-хешей исключены.
|
||||
Это однократные настольные wall-clock замеры, не норматив CPU и не оценка
|
||||
предельной дальности. Они не сравниваются напрямую с Lab048 на другой записи.
|
||||
|
||||
На 500 ксимв/с грубый поиск занял 0,492 с, строгая обработка — 54,586 с.
|
||||
Из 2374 кандидатов приняты 24; большинство остальных отклонено по magic
|
||||
PHY-заголовка. Новый ограниченный буфер **не устранил** стоимость ложных
|
||||
кандидатов. Увеличение Rs не делает этот приёмник быстрее реального времени.
|
||||
Факт Lab048 «15 с → 13,966 с» остаётся характеристикой прежней записи.
|
||||
|
||||
Пять тяжёлых проходов выполнены один раз. Первый запуск после сохранения
|
||||
замеров остановился на несовпадении API `build_packet/parse_packet` в модели
|
||||
очереди; вызовы исправлены и проверены тестами. Итоговая матрица построена
|
||||
с `--reuse-benchmarks`, без повторения RF/IQ-замеров. Режим проверяет скорости,
|
||||
параметры шума/порог и хеши восстановленных контрольных объектов.
|
||||
|
||||
## Память
|
||||
|
||||
Предел блока 65536 выборок. Для каждого Rs буфер ограничен выражением:
|
||||
`core + maximum_PHY_waveform + refinement_margin + left_margin + input_block`.
|
||||
Число кадров в сессии в формулу не входит. В тесте 100 последовательных
|
||||
блоков сохраняется тот же предел и не появляется история IQ.
|
||||
|
||||
| Rs, ксимв/с | Измеренный пик удерживаемых IQ, выборок | Проверяемый предел, выборок | IQ в пике, МиБ |
|
||||
|---:|---:|---:|---:|
|
||||
| 200 | 853027 | 853556 | 6,508 |
|
||||
| 400 | 493587 | 496316 | 3,766 |
|
||||
| 500 | 424309 | 424864 | 3,237 |
|
||||
| 800 | 308819 | 317696 | 2,356 |
|
||||
| 1000 | 279589 | 281968 | 2,133 |
|
||||
|
||||
С запасом на копирование, грубый и строгий поиск расчёт массивов даёт
|
||||
47,5–143,4 МиБ против исторической оценки Lab048 около 1,44 ГиБ.
|
||||
Это **расчёт массивов, не измеренный RSS процесса**: Python/SciPy, аллокатор,
|
||||
кэши и вызывающая программа добавляют память. История отчёта и заранее
|
||||
подготовленные 30 JPEG относятся к эксперименту, не к обязательному IQ-состоянию.
|
||||
|
||||
## Задержка и условие достижимости
|
||||
|
||||
Для каждого снимка JSON хранит capture, encode_complete, queue_enter,
|
||||
first_tx, last_tx, first_rx, object_complete, decode_complete и display.
|
||||
У не переданных/не допущенных стадий стоит null, причина удаления явна.
|
||||
QUEUE AGE=first_tx−queue_enter; RF AGE=last_tx−first_tx;
|
||||
END-TO-END IMAGE AGE=display−capture.
|
||||
|
||||
| Слагаемое | Статус |
|
||||
|---|---|
|
||||
| T_CAPTURE | Реальная экспозиция/считывание неизвестны; источник задан событиями, длительность моделируется нулём |
|
||||
| T_ENCODE | Измерено CPU-кодирование подготовленных изображений; C медиана около 0,143 мс; уменьшение/сдвиг вне этого замера |
|
||||
| T_QUEUE | Рассчитано существующим планировщиком; не измерение живой очереди |
|
||||
| T_RF | Рассчитано по настоящему формату; C на 500k — 92,256 мс |
|
||||
| T_RX_PROCESSING | Измерено в IQ-опыте, включая взаимодействие с буфером; в модели подставляется отдельно как стоимость пакета |
|
||||
| T_DECODE | Измерено CPU JPEG decode; C медиана около 0,187 мс; в общем IQ-замере уже включено, повторно туда не прибавляется |
|
||||
| T_DISPLAY | Экран не подключён; длительность моделируется нулём |
|
||||
|
||||
В конвейере RF и обработка ранних пакетов перекрываются. Нельзя прибавлять
|
||||
полный CPU service time к полному RF-времени и называть это измеренной
|
||||
сквозной задержкой. Прогноз без предыдущей очереди RX после last_tx:
|
||||
`lookahead + p + (N−1)×max(0,p−T_packet) + JPEG_decode`, где p — стоимость
|
||||
пакета. Формула независимо проверена последовательным расчётом 32 наборов
|
||||
событий и допуском по обе стороны точной границы возраста.
|
||||
|
||||
На 500k консервативный предел выдачи из буфера без стоимости CPU — 52,608 мс.
|
||||
Средняя измеренная стоимость полного RX-опыта — 2,298 с на переданный RF-пакет
|
||||
(включает фон и отвергнутых кандидатов, не только принятый FEC). При подстановке
|
||||
этой стоимости все 30 кадров C/5fps отклоняются ещё до TX при любом проверенном
|
||||
MAX_VIDEO_AGE; **FRESH DISPLAY FPS=0**. Это прогноз с измеренной стоимостью,
|
||||
не эксперимент с живым экраном.
|
||||
|
||||
Проверены также два явно условных варианта ускоренного RX:
|
||||
|
||||
| Условие | 5 кадров/с | 10 кадров/с |
|
||||
|---|---:|---:|
|
||||
| Выбранный JPEG | ≤1952 байт, 8 пакетов; C фактически ≤1893 | То же |
|
||||
| Выбранный Rs | 500 ксимв/с, 16 выборок/символ | То же |
|
||||
| Нижняя граница только по радио для 8 пакетов | 230,640 ксимв/с | 461,280 ксимв/с |
|
||||
| Предположенная стоимость RX-пакета | ≤16 мс | ≤12 мс |
|
||||
| Ускорение относительно данного замера | **≥143,64 раза** | **≥191,52 раза** |
|
||||
| MAX_VIDEO_AGE | 200 мс | 200 мс |
|
||||
| Расчётный возраст min/median/max | 192,447 / 192,474 / 192,513 мс | 160,447 / 160,474 / 160,513 мс |
|
||||
| Свежих в исходном окне | 30/6 с = **5** | 29/3 с = **9,667** |
|
||||
| С учётом опустошения | 30; без дополнительных | 30; один после 3 с |
|
||||
| Частота по интервалам последовательных показов | около 5 | около 10 |
|
||||
| FEASIBLE | **CONDITIONAL** | **CONDITIONAL** |
|
||||
|
||||
Для 10 кадров/с стартовая задержка превышает период камеры, поэтому первый
|
||||
трёхсекундный отчёт честно даёт 9,667, а не 10 с включением позднего кадра.
|
||||
Частота около 10 относится к установившимся интервалам. Условия не означают,
|
||||
что такое ускорение достигнуто или достижимо только небольшой правкой.
|
||||
Живые capture/display, входной jitter, потери и запас CPU ещё должны уложиться
|
||||
в оставшийся бюджет. Для 5 кадров/с запас возраста этого примера лишь около 7,5 мс.
|
||||
|
||||
Итак, **по пропускной способности BPSK достаточно**, без QPSK и нового FEC;
|
||||
**с нынешним измеренным приёмником — нет**. Аппаратная работоспособность
|
||||
HackRF на этих Rs в данном задании не проверялась. 3 кадра/с остаются
|
||||
промежуточным уровнем, цель проекта >3, ориентиры 5 и 10.
|
||||
|
||||
## ELRS и следующий опыт
|
||||
|
||||
CONTROL LINK OK/LOST — внешнее состояние для контроллера ровера. Видеопланировщик
|
||||
вообще не принимает его как радиопакет. Проверка на существующем TwoStageFailsafe:
|
||||
при отсутствии новых команд 100 мс → замедление, 200 мс → торможение,
|
||||
а результат видеомодели остаётся тем же. Это проверка разделения моделей,
|
||||
не готовое сопряжение CRSF. Safety/session/emergency-модули не менялись.
|
||||
|
||||
Рекомендуемый Lab051 — программное профилирование и ускорение строгого
|
||||
обработчика ложных кандидатов на сохранённом детерминированном сценарии
|
||||
C/500k, с отдельной проверкой фоновых ложных принятий и неизменными FEC/CRC.
|
||||
Измерять стоимость уточнения, оценки частоты, заголовка и FEC; сохранить
|
||||
независимость от TX и корректность на границах блоков. Не объявлять 5 кадров/с
|
||||
до полного непрерывного опыта со свежими JPEG и устойчивым временем обработки.
|
||||
RF, QPSK, новый кодек, 12+3 FEC и перенос на 180 МГц этим этапом не разрешаются.
|
||||
|
||||
## Воспроизведение
|
||||
|
||||
```powershell
|
||||
python experiments/lab050_video_stream.py
|
||||
# Только пересчёт матрицы с уже сохранёнными локальными замерами IQ:
|
||||
python experiments/lab050_video_stream.py --reuse-benchmarks
|
||||
python -m pytest tests/test_lab050_video_stream.py -q -p no:cacheprovider
|
||||
python tools/quick_gate.py
|
||||
python -m pytest tests/ -q
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Полный первый запуск занимает минуты; новые RF-сеансы в нём отсутствуют.
|
||||
[result.json](../data/processed/lab050/result.json) содержит 240 сценариев,
|
||||
все временные отметки, профили, расчёты ёмкости и отдельные условные модели.
|
||||
[receiver_benchmarks.json](../data/processed/lab050/receiver_benchmarks.json)
|
||||
содержит исходные пять IQ-замеров. Большие IQ-файлы не создавались.
|
||||
|
||||
44 целевых теста проходят. Verifier/adversary подтвердили исправление
|
||||
поздно завершённого старого кадра; verifier независимо проверил границы
|
||||
ёмкости, 60 сценариев очереди, 32 прогноза RX и арифметику IQ-замеров.
|
||||
Итог: quick_gate PASS — 650 тестов и 14 функциональных проверок;
|
||||
полный pytest — 650 passed за 78,73 с, одно предупреждение о доступе
|
||||
к `.pytest_cache`; git diff --check PASS. Подробности — в записи 039 журнала.
|
||||
|
||||
LAB050 GIT ADD = NOT PERFORMED. LAB050 COMMIT = NOT PERFORMED.
|
||||
PUSH = NOT PERFORMED. RF TX = 0. STOP.
|
||||
281
docs/lab051_current_state.md
Normal file
281
docs/lab051_current_state.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Lab051 — вычислительная архитектура потокового BPSK-приёмника
|
||||
|
||||
Дата: 8 сентября 2026 года. SOFTWARE ONLY. RF TX = 0.
|
||||
|
||||
## Результат и граница вывода
|
||||
|
||||
**Программный эксперимент завершён; цели реального времени и свежести не достигнуты.**
|
||||
В полном шестисекундном потоке получены 240/240 физических пакетов и 30/30 JPEG
|
||||
с точным содержимым. Обработка 6,044416 с синтетического IQ заняла **38,630132 с**,
|
||||
отношение **6,391045**. При MAX_VIDEO_AGE=200 мс **FRESH DISPLAY FPS=0**;
|
||||
не выполнен даже минимальный критерий >0.
|
||||
|
||||
Короткий шумный сценарий Lab050 ускорен примерно в **48,21 раза** относительно
|
||||
исторического замера: 55,157553 с → медиана **1,144176 с** трёх проходов.
|
||||
Его processing/RF = **3,534193**, поэтому и этот вариант не работает в реальном времени.
|
||||
Среднее успешного кандидата — **17,7675 мс**; полная стоимость короткого опыта —
|
||||
**47,674 мс/пакет**, а длинного потока — **160,959 мс/пакет**.
|
||||
Последние величины включают обработку ложных кандидатов и фона.
|
||||
|
||||
Модуляция BPSK, PRBS7, PHY-заголовок, фрагментация, интерливер, FEC R=1/2 K=7,
|
||||
171/133 octal, soft-метрика и CRC не менялись. GPU и multiprocessing не использовались.
|
||||
Новых зависимостей нет. Аппаратура не перечислялась и не запускалась.
|
||||
|
||||
## Git и аудит до реализации
|
||||
|
||||
Исходный main: `cdf5396e807b83f82a264632dc46f71428fcd616`.
|
||||
Проверены HEAD/status/diff stat/diff check, затем показан точный staged scope.
|
||||
По прямому разрешению пункта 0 задания создан коммит:
|
||||
|
||||
`60691a09f193f36983c7174c5252ed0897c559e9`
|
||||
|
||||
`Lab050: model fresh-frame SDR video stream`
|
||||
|
||||
В него вошли ровно девять файлов: PROJECT_LOG.md, docs/LABS_INDEX.md,
|
||||
experiments/lab046c_offline_recovery.py, protocol/video_frame_scheduler.py,
|
||||
docs/lab050_current_state.md, experiments/lab050_streaming_receiver.py,
|
||||
experiments/lab050_video_stream.py, tests/test_lab050_video_stream.py,
|
||||
data/processed/lab050/receiver_benchmarks.json. 1769 добавлений, 12 удалений.
|
||||
Большой result.json, профили JPEG и посторонние локальные файлы не добавлялись.
|
||||
|
||||
Lab051 начата от этого HEAD. Изучены Lab046B/C, Lab047, Lab048, Lab050,
|
||||
Lab043/044/045 и свёрточный декодер. Ядро protocol в Lab051 не менялось.
|
||||
Планировщик LATEST_ONLY и ObjectAssembler переиспользованы. Новая видеочередь не создана.
|
||||
|
||||
## Профиль до оптимизации
|
||||
|
||||
Рабочая точка: 500000 символов/с, Fs=8000000 отсчётов/с, 16 отсчётов/символ.
|
||||
Профиль C: 128×96 RGB, JPEG quality 25. Короткий контроль: три изображения,
|
||||
24 пакета, seed=50050, coded-bit Eb/N0=7 дБ, CFO=+10 Гц, фаза 0,73 рад,
|
||||
чередование полярности, исходное CS8-квантование. Генератор точно прежний, из Lab050.
|
||||
|
||||
Путь кандидата: IQ → грубая нормированная корреляция → уточнение старта →
|
||||
согласованный RRC-фильтр → timing → оценка частотного рассогласования CFO →
|
||||
компенсация CFO и фазы → мягкие решения → PHY header → deinterleave →
|
||||
soft Viterbi → protocol parser → CRC. Только полный PHY/FEC/CRC PASS выдаёт пакет.
|
||||
TX-границы, ожидаемое число кадров и содержимое не входят в RX API.
|
||||
|
||||
Историческая база Lab050: 55,157553 / 0,323745 = 170,373647.
|
||||
Свежий инструментированный baseline Lab051: **49,614327 с**, processing/RF=153,251430.
|
||||
Настольная нагрузка и инструментация влияют на время; эти замеры не подменяют друг друга.
|
||||
Все 2374 кандидата: 24 valid, 2350 false. Средние времена — **72,967 / 20,099 мс**.
|
||||
|
||||
**Главная причина замедления — fallback timing.** Номинальное положение равно 416,
|
||||
а диапазон остался −800…+800 отсчётов. Начало выходит за ноль, и прежний быстрый
|
||||
путь возвращается к Python-перебору 1601 смещения. У ложных кандидатов timing
|
||||
занял 41,833 с из 47,232 с строгой обработки. Их matched filter — 0,934 с,
|
||||
уточнение — 2,797 с. Прежняя реализация уже останавливала кандидата при PHY FAIL:
|
||||
тело, Viterbi и CRC на таких входах не запускались.
|
||||
|
||||
Для преамбулы и заголовка достаточно 23456 входных отсчётов и 23616 отсчётов
|
||||
после RRC. Полный физический пакет с payload 256 байт требует 92256 входных
|
||||
отсчётов. Длина тела определяется только проверенным PHY-заголовком.
|
||||
|
||||
## Изменения вычислений
|
||||
|
||||
1. Децимация сохраняет внутреннюю часть конечного импульсного отклика FIR.
|
||||
При сдвиге буфера заново считаются левый нулевой край и новые правые отсчёты.
|
||||
Коэффициенты resample_poly/kaiser(8) прежние; сравнение с исходной функцией
|
||||
побитно точное. Среднее по всему прежнему окну сохраняется. Корреляция считается
|
||||
только для принадлежащих блоку стартов плюс правый сосед максимума.
|
||||
2. Корреляция известного prefix для уточнения считается один раз для общего участка.
|
||||
Кандидаты используют срезы, с прежней поправкой локального DC.
|
||||
Постоянный probe и неразделимые FFT-пики сохраняют исходный выбор первого
|
||||
максимума; при необходимости вызывается старое уточнение.
|
||||
3. Компенсация известного +50 кГц и RRC выполняются для общего участка.
|
||||
Локальное вычитание среднего и оба нулевых края изолированного окна
|
||||
восстановлены отдельно. Без этих поправок общая фильтрация меняла бы алгоритм.
|
||||
Тело за пределами общего участка фильтруется локально только после PHY PASS.
|
||||
4. Извлечение символов кэшируется по уточнённому положению и длине внутри одного блока.
|
||||
Усечённое окно не подменяется ранее сохранённым полным окном.
|
||||
Кэш уничтожается вместе с блоком; история сессии не сохраняется.
|
||||
5. Timing сохраняет весь прежний диапазон 1601 смещения. Корреляция с разреженной
|
||||
известной преамбулой и разности накопленных энергий заменяют Python-перебор.
|
||||
Тело за последним нужным отсчётом не участвует в этом расчёте. При пиках,
|
||||
неразделимых с точностью float32, возвращается исходный расчёт и его tie-правило.
|
||||
6. CFO по прежним преамбульным символам не изменён. Векторы переноса кэшируются;
|
||||
вектор остаточной частоты строится на символах локального окна. Gain/фаза для
|
||||
probe и полного кадра не объединены: у них различается локальное среднее IQ.
|
||||
7. Viterbi после устранения главного повтора остался существенной долей принятого
|
||||
пакета. Предвычислены четыре различные пары евклидовых branch metrics;
|
||||
две ветви сравниваются непосредственно, с прежним выбором branch 0 при равенстве.
|
||||
Решётка, старт/финиш 0, шесть хвостовых битов и декодированные биты сохранены.
|
||||
|
||||
Дополнительный порог преамбулы/новый критерий отсева не введён. Фиксированный
|
||||
порог первого каскада **0.030964473262429237** сохранён. В коротком опыте совпали
|
||||
число кандидатов и все причины отказа с исходным RX. Параллелизм и CUDA не добавлялись.
|
||||
|
||||
## Профиль стадий
|
||||
|
||||
Ячейка: **число вызовов / суммарные миллисекунды** по соответствующей группе.
|
||||
Это не длительность одного вызова. Общая работа front-end учитывается в том
|
||||
кандидате, который впервые запросил её. Фаза показана без вложенного CFO;
|
||||
время body pipeline не складывается повторно со временем Viterbi/CRC.
|
||||
|
||||
| Стадия | Baseline valid (24) | Optimized valid (24) | Baseline false (2350) | Optimized false (2350) |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Точное уточнение | 24 / 28.974 | 24 / 93.878 | 2350 / 2797.404 | 2350 / 119.743 |
|
||||
| Согласованная RRC-фильтрация | 48 / 190.656 | 48 / 102.875 | 2336 / 933.965 | 174 / 26.502 |
|
||||
| Timing: весь диапазон | 48 / 914.632 | 48 / 43.535 | 2336 / 41833.166 | 174 / 138.588 |
|
||||
| CFO и комплексное усиление | 48 / 10.838 | 48 / 8.442 | 2336 / 485.772 | 174 / 26.385 |
|
||||
| Фаза/полярность и диагностика | 48 / 8.994 | 48 / 8.324 | 2336 / 424.756 | 174 / 28.438 |
|
||||
| PHY header | 48 / 1.711 | 48 / 1.452 | 2298 / 67.136 | 2298 / 32.044 |
|
||||
| Viterbi | 24 / 484.381 | 24 / 132.119 | 0 / 0 | 0 / 0 |
|
||||
| Protocol parser / CRC | 24 / 0.270 | 24 / 0.211 | 0 / 0 | 0 / 0 |
|
||||
|
||||
Грубая локализация — общая стадия: 36 вызовов, baseline 498,040 мс,
|
||||
optimized **89,183 мс**. На один false candidate среднее **0,187317 мс**,
|
||||
медиана **0,060500 мс**; это включает cache hits. Холодное новое окно дороже.
|
||||
|
||||
## Массивы и память
|
||||
|
||||
Число всех аллокаций внутри NumPy/SciPy C не измерено. Line tracer наблюдает
|
||||
массивы в локальных переменных проектного Python: число ниже — **нижняя граница**,
|
||||
включающая views. Владельцы данных выделены отдельно. Временные выражения и
|
||||
внутренние C-массивы не перечислены. Отдельный tracemalloc-проход измеряет пик
|
||||
отслеживаемой памяти. Эти медленные проходы не включены в CPU benchmark.
|
||||
|
||||
| Кандидат / алгоритм | Наблюдаемые ndarray | Из них владельцы | Суммарные байты наблюдаемых владельцев | Пик tracemalloc, байт |
|
||||
|---|---:|---:|---:|---:|
|
||||
| false / baseline | 2888 | 61 | 6612268 | 2104129 |
|
||||
| false / optimized | 76 | 61 | 5906101 | 5524392 |
|
||||
| valid / baseline | 14361 | 8704 | 16681890 | 5885299 |
|
||||
| valid / optimized | 6594 | 6560 | 12345244 | 7771675 |
|
||||
|
||||
Пик холодного кандидата вырос из-за общих промежуточных данных: ускорение не
|
||||
объявляется уменьшением каждого временного массива. В рабочем потоке:
|
||||
|
||||
- Пик удерживаемых IQ: **3177728 байт = 3,031 МиБ**.
|
||||
- Проверяемый предел IQ: 3398912 байт; кэш децимации хранится отдельно.
|
||||
- Инженерная оценка рабочих массивов: **136,161 МиБ**. Использован запас прежних
|
||||
22 размеров IQ-окна плюс 20 размеров на новые массивы и кэши. Это оценка для
|
||||
данного опыта, не доказанный универсальный предел всех аллокаций и не RSS.
|
||||
- Windows process RSS после потока: **154,105 МиБ**; peak RSS процесса —
|
||||
**178,004 МиБ**. В него входят Python, библиотеки, генератор, предыдущие замеры и аллокатор.
|
||||
- Текущий объект ограничен 65536 байтами / 269 фрагментами; C фактически ≤1893 байт.
|
||||
|
||||
Длинный тест: **4096 блоков, 33,554432 с модельного времени**. Фон нулевой;
|
||||
шум отдельно проверен в основном опыте. После прогрева удерживаются 2653440 байт
|
||||
IQ и 397216 байт децимированных данных; значения не растут.
|
||||
RSS контрольных точек: **156,199–156,250 МиБ**, без роста вместе с длительностью.
|
||||
После finish эти буферы освобождены. История JSON относится к отчёту, не к RX.
|
||||
|
||||
## Непрерывный поток 5 изображений/с
|
||||
|
||||
30 воспроизводимых сдвигов исходного data/raw/lab011_scene.jpg, профиль C,
|
||||
1740–1893 байта. Это искусственная последовательность, не видео камеры.
|
||||
Видеопланировщик LATEST_ONLY использует время capture и завершает активное JPEG.
|
||||
Передатчик знает нижнюю задержку радио/буфера; он не получает будущую стоимость
|
||||
CPU или результаты приёма. Приёмник получает лишь IQ-блоки по 65536 отсчётов.
|
||||
|
||||
Шум, фаза, CFO и полярность заданы, но готовность JPEG берётся из измеренного encode.
|
||||
Поэтому точное размещение пакетов и число фоновых кандидатов длинного опыта могут
|
||||
немного меняться между запусками. Короткий baseline использует точно прежний
|
||||
детерминированный сценарий Lab050.
|
||||
|
||||
Время CPU-работника для каждого блока:
|
||||
`start=max(previous_end, block_arrival); end=start+measured_push_time`.
|
||||
Сборка/JPEG decode добавляют своё измеренное время. Генерация TX/IQ, SHA-сравнение
|
||||
с передатчиком и запись JSON исключены. Длительность RX не заменена нулём.
|
||||
Все данные реально прошли независимый PHY/FEC/CRC и JPEG decode.
|
||||
|
||||
| Метрика | Итог |
|
||||
|---|---:|
|
||||
| INPUT FPS / FRAMES | 5 / 30 |
|
||||
| TX IMAGES / PHYSICAL PACKETS | 30 / 240 |
|
||||
| DROPPED BEFORE TX | 0 |
|
||||
| MAX WAITING VIDEO FRAMES | 1 |
|
||||
| RX PHYSICAL PACKETS / IMAGES | 240 / 30 |
|
||||
| JPEG SHA MATCH | 30 / 30 |
|
||||
| DROPPED STALE AT DISPLAY | 30 |
|
||||
| DISPLAYED / FRESH DISPLAYED | 0 / 0 |
|
||||
| RAW RECEIVED FPS в первых 6 с CPU-шкалы | 0.833333 |
|
||||
| RAW DISPLAY FPS / FRESH DISPLAY FPS | 0 / 0 |
|
||||
| Возраст декодированных JPEG: min / median / max | 276.515 / 15691.203 / 31150.009 мс |
|
||||
| MAX_VIDEO_AGE | 200 мс |
|
||||
| Строгих кандидатов | 147082 |
|
||||
| Полное время обработки / IQ | 38.630132 / 6.044416 с |
|
||||
| PROCESSING / RF TIME | 6.391045 |
|
||||
|
||||
Возраст относится к **декодированным, затем отброшенным JPEG**. Показанных изображений нет.
|
||||
RAW DISPLAY и FRESH DISPLAY совпадают, потому что политика не выводит просроченные
|
||||
изображения; RAW RECEIVED приведён отдельно.
|
||||
|
||||
Физическое время одного JPEG C: 8×5766/500000 = **92,256 мс**. Очередь ожидающих
|
||||
JPEG в этом режиме не задерживает готовые изображения. Время RX за опыт:
|
||||
38,619085 с; сборка 5,451 мс; JPEG decode 5,596 мс суммарно на 30 JPEG.
|
||||
RF и CPU перекрываются по событийной шкале: эти суммы нельзя просто сложить
|
||||
и назвать измеренной задержкой камеры→экрана. Camera capture latency и display
|
||||
latency **UNKNOWN**; экран в отчётной модели мгновенный.
|
||||
|
||||
Capture lookup используется только **после** независимой сборки JPEG для метрики
|
||||
возраста/решения модели вывода. В существующем fragment format нет времени съёмки.
|
||||
Готовый независимый аппаратный контроль возраста этим не доказан; формат в Lab051
|
||||
не расширялся. ID монотонны в одной конечной сессии; wrap/restart не реализованы.
|
||||
|
||||
Внешняя IQ-очередь также не реализована. Офлайн-источник подаёт следующий блок
|
||||
по запросу и физически не хранит многосекундный backlog. Если бы вход шёл непрерывно,
|
||||
максимальное отставание **32,632404 с** потребовало бы около **2088473848 байт**
|
||||
дополнительного complex64 IQ. Поэтому bounded RX state нельзя выдавать за готовую
|
||||
ограниченную live-систему: при таких затратах потребуются потери/переполнение.
|
||||
REAL-TIME CAPABLE = NO.
|
||||
|
||||
В длительном опыте встречено значительно больше фоновых кандидатов, чем в коротком:
|
||||
147082 против 2374. Кэш дал 135057 повторных использований, но осталось 12239 новых
|
||||
окон синхронизации. Следующее ограничение — стоимость этих фоновых событий.
|
||||
Одного ускорения Viterbi недостаточно; подгонять отсев по TX запрещено.
|
||||
|
||||
## Регрессии и независимая проверка
|
||||
|
||||
38 целевых тестов: весь диапазон timing, ненулевые и нулевые ties, локальные
|
||||
DC/края общего RRC, точная децимация при движении окна, Viterbi noisy/tie/invalid,
|
||||
совпадение старого/нового блочного приёма, усечение, буферы и finally.
|
||||
Verifier нашёл два случая округления FFT (timing и refine); оба исправлены,
|
||||
воспроизведены повторно и закреплены тестами. Adversary отдельно проверил
|
||||
malformed PHY (0 body/Viterbi), дубликаты и старые ID, лимиты объекта,
|
||||
усечение и восстановление глобальных подмен.
|
||||
|
||||
Сохранённая Lab047-запись 20 ксимв/с повторно обработана штатным Lab048:
|
||||
**36/36, FEC/CRC PASS, JPEG SHA MATCH**, 19 ошибок до FEC, 0 после.
|
||||
Исходный путь и SHA JPEG сохранены в regression20k/result.json.
|
||||
Старый режим работает прежним декодером; адаптер Lab051 принимает только 500 ксимв/с
|
||||
и не подменяет аппаратную проверку этого режима.
|
||||
|
||||
Итог: quick_gate PASS — 688 тестов, 17 функциональных проверок, 13 штатных пропусков.
|
||||
Полный pytest — 688 passed за 72,92 с, 0 failed; одно предупреждение доступа к
|
||||
`.pytest_cache`. git diff --check PASS. Подробности — в записи 040 журнала.
|
||||
Управление ELRS/CRSF, локальная защита и ядро protocol не менялись.
|
||||
|
||||
## Файлы и воспроизведение
|
||||
|
||||
- [Потоковый RX](../experiments/lab051_streaming_receiver.py)
|
||||
- [Профилировщик](../experiments/lab051_receiver_profile.py)
|
||||
- [Сценарии и событийная шкала](../experiments/lab051_receiver_performance.py)
|
||||
- [Тесты](../tests/test_lab051_streaming_receiver.py)
|
||||
- [Baseline](../data/processed/lab051/baseline_profile.json)
|
||||
- [Три коротких замера и профиль](../data/processed/lab051/benchmark.json)
|
||||
- [Основной поток и все 739 блоков](../data/processed/lab051/stream.json)
|
||||
- [Длинный тест памяти](../data/processed/lab051/memory.json)
|
||||
- [Наблюдение массивов](../data/processed/lab051/replay.json)
|
||||
- [20 ксимв/с](../data/processed/lab051/regression20k/result.json)
|
||||
|
||||
```powershell
|
||||
python experiments/lab051_receiver_profile.py
|
||||
python experiments/lab051_receiver_performance.py --stage all
|
||||
python -m pytest tests/test_lab051_streaming_receiver.py -q -p no:cacheprovider
|
||||
python tools/quick_gate.py
|
||||
python -m pytest tests/ -q
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Старая запись:
|
||||
|
||||
```powershell
|
||||
python experiments/lab048_receiver_performance.py --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/lab051/regression20k
|
||||
```
|
||||
|
||||
Локальные replay .npy — ограниченные окна, не полная IQ-запись; в Git не добавлены.
|
||||
Новый полный CS8/IQ файл не создавался. Lab051 git add/commit и push не выполнялись.
|
||||
Следующий рекомендуемый этап — отдельное программное исследование стоимости
|
||||
уникальных фоновых кандидатов в полном режиме 5 кадров/с с тем же PHY/FEC/CRC.
|
||||
RF, QPSK, новый FEC/кодек, 180 МГц и аппаратные 500 ксимв/с не выполнялись. STOP.
|
||||
390
docs/lab052_current_state.md
Normal file
390
docs/lab052_current_state.md
Normal 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 в решении не участвуют.
|
||||
На меняющемся фоне и в слабых положительных регрессиях дополнительные потери
|
||||
не наблюдались; универсальная чувствительность при любом нестационарном канале
|
||||
этим конечным исследованием не доказана.
|
||||
|
||||

|
||||
|
||||
## Выбранное правило
|
||||
|
||||
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.
|
||||
@@ -639,10 +639,13 @@ def decode_candidate_from_memmap(
|
||||
sample_count: int,
|
||||
candidate: CoarseCandidate,
|
||||
detection_number: int,
|
||||
*,
|
||||
read_iq_window: Callable | None = None,
|
||||
) -> CandidateOutcome:
|
||||
"""Строго проверить один candidate локальными IQ-окнами."""
|
||||
|
||||
started = time.perf_counter()
|
||||
window_reader = lab046c._cs8_window if read_iq_window is None else read_iq_window
|
||||
reference = lab046c.lab044._preamble_reference_samples()
|
||||
probe_symbols = lab046c.lab045.PREAMBLE_BIT_COUNT + lab046c.lab045.PHY_HEADER_BIT_COUNT
|
||||
probe_waveform = (
|
||||
@@ -655,7 +658,7 @@ def decode_candidate_from_memmap(
|
||||
sample_count,
|
||||
candidate.sample_offset + probe_waveform + REFINEMENT_MARGIN_SAMPLES,
|
||||
)
|
||||
probe_iq = lab046c._cs8_window(raw, probe_start, probe_end)
|
||||
probe_iq = window_reader(raw, probe_start, probe_end)
|
||||
maximum_window = int(probe_iq.size)
|
||||
local_coarse = candidate.sample_offset - probe_start
|
||||
try:
|
||||
@@ -740,7 +743,7 @@ def decode_candidate_from_memmap(
|
||||
maximum_window=maximum_window,
|
||||
started=started,
|
||||
)
|
||||
full_iq = lab046c._cs8_window(raw, refined_global, full_end)
|
||||
full_iq = window_reader(raw, refined_global, full_end)
|
||||
maximum_window = max(maximum_window, int(full_iq.size))
|
||||
full = lab046b._extract_soft_window(full_iq, 0, frame_symbols)
|
||||
if not full.passed:
|
||||
|
||||
163
experiments/lab050_streaming_receiver.py
Normal file
163
experiments/lab050_streaming_receiver.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""Lab050: независимый приём IQ-блоков с ограниченной памятью, без RF.
|
||||
|
||||
Использует корреляцию и строгий PHY/FEC/CRC Lab048. Принятые пакеты сразу
|
||||
возвращаются вызывающему коду; история IQ/пакетов внутри не накапливается.
|
||||
Адаптер констант старых лабораторий рассчитан на последовательный вызов.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from experiments import lab046c_offline_recovery as offline
|
||||
from experiments import lab049_bpsk_symbol_rate as phy
|
||||
from experiments.lab049_independent_iq import receiver_mode
|
||||
|
||||
|
||||
@contextmanager
|
||||
def streaming_mode(mode: phy.PhyMode):
|
||||
"""Оставить не менее двух выборок/символ в грубом поиске."""
|
||||
module = phy.lab045.lab044.lab043
|
||||
original = module.DETECTION_DECIMATION
|
||||
with receiver_mode(mode):
|
||||
module.DETECTION_DECIMATION = min(20, mode.samples_per_symbol // 2)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
module.DETECTION_DECIMATION = original
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReceivedPacket:
|
||||
start_sample: int
|
||||
end_sample: int
|
||||
emitted_after_sample: int
|
||||
payload: bytes
|
||||
|
||||
|
||||
class StreamingReceiver:
|
||||
"""push_iq_block выдаёт пакеты до конца потока; finish только опустошает хвост.
|
||||
|
||||
Размер кадра для удержания IQ определяется максимальной допустимой PHY
|
||||
length (1024 байта), а не TX-границами/числом ожидаемых пакетов.
|
||||
Порог Lab047 остаётся фиксированным. Источник и размеры объекта неизвестны.
|
||||
"""
|
||||
|
||||
def __init__(self, symbol_rate: int, *, block_samples: int = 65_536):
|
||||
self.mode = phy.PhyMode(symbol_rate)
|
||||
if self.mode.samples_per_symbol < 4:
|
||||
raise ValueError("Для этого адаптера нужно не менее четырёх выборок/символ")
|
||||
if type(block_samples) is not int or block_samples < 1024:
|
||||
raise ValueError("Размер блока должен быть целым и не меньше 1024")
|
||||
self.block_samples = block_samples
|
||||
self.decimation = min(20, self.mode.samples_per_symbol // 2)
|
||||
self.core_samples = math.ceil(block_samples / self.decimation) * self.decimation
|
||||
margin = offline.REFINEMENT_MARGIN_SAMPLES
|
||||
self.margin = math.ceil(margin / self.decimation) * self.decimation
|
||||
maximum_symbols = (phy.lab045.PREAMBLE_BIT_COUNT + phy.lab045.PHY_HEADER_BIT_COUNT
|
||||
+ phy.lab045.conv.encoded_bit_count(phy.lab045.MAX_PACKET_BYTES * 8)
|
||||
+ 2 * phy.lab045.GUARD_SYMBOL_COUNT + phy.lab045.RRC_SPAN_SYMBOLS)
|
||||
self.lookahead = maximum_symbols * self.mode.samples_per_symbol + self.margin
|
||||
self.buffer_limit_samples = self.core_samples + self.lookahead + self.margin + block_samples
|
||||
self.buffer = np.empty(0, np.complex64)
|
||||
self.base_sample = self.scan_sample = self.total_samples = self.accepted_until = 0
|
||||
self.peak_buffer_samples = self.peak_candidate_count = 0
|
||||
self.packet_count = self.candidate_count = self.blocks_processed = 0
|
||||
self.processing_seconds = self.coarse_seconds = self.strict_seconds = 0.0
|
||||
self.score_count = 0
|
||||
self.score_sum = 0.0
|
||||
self.rejected_reasons: Counter = Counter()
|
||||
self.closed = False
|
||||
with streaming_mode(self.mode):
|
||||
self.reference = offline.lab046b._decimate_then_downconvert(
|
||||
phy.lab045.lab044._preamble_reference_samples()).copy()
|
||||
|
||||
@property
|
||||
def memory_estimate_bytes(self) -> int:
|
||||
"""Консервативная оценка массивов, не измеренный RSS Python/SciPy."""
|
||||
window_bytes = self.buffer_limit_samples * np.dtype(np.complex64).itemsize
|
||||
# Два буфера при копировании + прежний коэффициент временных массивов
|
||||
# Lab048 + запас для строгого matched filter и индексов кандидатов.
|
||||
return 2 * window_bytes + 12 * window_bytes + 8 * window_bytes + self.reference.nbytes
|
||||
|
||||
def _scan(self, owned_end: int) -> list[ReceivedPacket]:
|
||||
started = time.perf_counter()
|
||||
scores = offline._correlation_scores(self.buffer, self.reference)
|
||||
self.coarse_seconds += time.perf_counter() - started
|
||||
indexes = offline._all_local_peak_indexes(scores)
|
||||
offsets = self.base_sample + indexes * self.decimation
|
||||
indexes = indexes[(offsets >= self.scan_sample) & (offsets < owned_end)]
|
||||
self.score_count += len(indexes)
|
||||
self.score_sum += float(np.sum(scores[indexes], dtype=np.float64))
|
||||
indexes = indexes[scores[indexes] >= np.float32(phy.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD)]
|
||||
self.peak_candidate_count = max(self.peak_candidate_count, len(indexes))
|
||||
emitted = []
|
||||
for index in indexes:
|
||||
absolute = self.base_sample + int(index) * self.decimation
|
||||
if absolute < self.accepted_until:
|
||||
continue
|
||||
self.candidate_count += 1
|
||||
started = time.perf_counter()
|
||||
outcome = offline.decode_candidate_from_memmap(
|
||||
self.buffer.view(np.float32), self.buffer.size,
|
||||
offline.CoarseCandidate(int(index) * self.decimation, float(scores[index])),
|
||||
self.candidate_count,
|
||||
read_iq_window=lambda raw, start, end: raw[2*start:2*end].view(np.complex64),
|
||||
)
|
||||
self.strict_seconds += time.perf_counter() - started
|
||||
if outcome.accepted and outcome.crc == phy.lab045.CRC_PASS and outcome.frame is not None:
|
||||
end = self.base_sample + outcome.next_sample
|
||||
self.accepted_until = end
|
||||
self.packet_count += 1
|
||||
emitted.append(ReceivedPacket(
|
||||
self.base_sample + outcome.refined_start_sample, end, self.total_samples,
|
||||
bytes(outcome.frame.reception.payload_bytes),
|
||||
))
|
||||
else:
|
||||
self.rejected_reasons[outcome.reason] += 1
|
||||
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 push_iq_block(self, samples: np.ndarray) -> list[ReceivedPacket]:
|
||||
if self.closed:
|
||||
raise RuntimeError("Поток уже закрыт")
|
||||
samples = np.asarray(samples, dtype=np.complex64)
|
||||
if samples.ndim != 1 or samples.size > self.block_samples or not np.all(np.isfinite(samples)):
|
||||
raise ValueError("Нужен конечный одномерный IQ-блок не больше block_samples")
|
||||
started = time.perf_counter()
|
||||
self.total_samples += samples.size
|
||||
self.buffer = np.concatenate((self.buffer, samples))
|
||||
self.peak_buffer_samples = max(self.peak_buffer_samples, self.buffer.size)
|
||||
if self.buffer.size > self.buffer_limit_samples:
|
||||
raise RuntimeError("Превышен предел буфера IQ")
|
||||
emitted = []
|
||||
with streaming_mode(self.mode):
|
||||
while self.total_samples >= self.scan_sample + self.core_samples + self.lookahead:
|
||||
emitted.extend(self._scan(self.scan_sample + self.core_samples))
|
||||
self.processing_seconds += time.perf_counter() - started
|
||||
return emitted
|
||||
|
||||
def finish(self) -> list[ReceivedPacket]:
|
||||
if self.closed:
|
||||
raise RuntimeError("Поток уже закрыт")
|
||||
started = time.perf_counter()
|
||||
emitted = []
|
||||
with streaming_mode(self.mode):
|
||||
if self.scan_sample < self.total_samples:
|
||||
emitted = self._scan(self.total_samples)
|
||||
self.buffer = np.empty(0, np.complex64)
|
||||
self.closed = True
|
||||
self.processing_seconds += time.perf_counter() - started
|
||||
return emitted
|
||||
470
experiments/lab050_video_stream.py
Normal file
470
experiments/lab050_video_stream.py
Normal file
@@ -0,0 +1,470 @@
|
||||
"""Lab050: JPEG-профили, свежесть видеопотока и блочный RX без оборудования."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from dataclasses import asdict, dataclass
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
|
||||
if __package__ in (None, ""):
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
from experiments import lab046_bpsk_object_transport as transport
|
||||
from experiments import lab049_bpsk_symbol_rate as phy
|
||||
from experiments.lab050_streaming_receiver import StreamingReceiver
|
||||
from protocol import packet as packet_protocol
|
||||
from protocol.link_packet import Direction, LinkPacket, TrafficClass, HEADER_SIZE
|
||||
from protocol.video_age_policy import AgePolicyPacket
|
||||
from protocol.video_frame_scheduler import FramePolicy, VideoFrameGroup, schedule_video_frames
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "data/processed/lab050"
|
||||
SYMBOL_RATES = (200_000, 400_000, 500_000, 800_000, 1_000_000)
|
||||
INPUT_RATES = (3, 5, 10)
|
||||
AGE_LIMITS = (0.200, 0.333, 0.500, 1.000)
|
||||
FRAME_INTERVALS = 5766
|
||||
FRAGMENT_BYTES = transport.FRAGMENT_DATA_SIZE
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JpegProfile:
|
||||
name: str
|
||||
width: int
|
||||
height: int
|
||||
color_mode: str
|
||||
quality: int
|
||||
|
||||
|
||||
PROFILES = (
|
||||
JpegProfile("A", 320, 240, "RGB", 35),
|
||||
JpegProfile("B", 160, 120, "RGB", 35),
|
||||
JpegProfile("C", 128, 96, "RGB", 25),
|
||||
JpegProfile("D", 160, 120, "L", 30),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EncodedImage:
|
||||
image_id: int
|
||||
jpeg: bytes
|
||||
encode_seconds: float
|
||||
decode_seconds: float
|
||||
psnr_db: float
|
||||
|
||||
|
||||
def fragment_count(jpeg_bytes: int) -> int:
|
||||
if type(jpeg_bytes) is not int or jpeg_bytes <= 0:
|
||||
raise ValueError("Размер JPEG должен быть положительным целым")
|
||||
return (jpeg_bytes + FRAGMENT_BYTES - 1) // FRAGMENT_BYTES
|
||||
|
||||
|
||||
def maximum_jpeg_bytes(symbol_rate: int, fps: int) -> int:
|
||||
"""Точный ступенчатый предел с добивкой последнего физического кадра."""
|
||||
phy.PhyMode(symbol_rate)
|
||||
if type(fps) is not int or fps <= 0:
|
||||
raise ValueError("Частота изображений должна быть положительным целым")
|
||||
return (symbol_rate // (fps * FRAME_INTERVALS)) * FRAGMENT_BYTES
|
||||
|
||||
|
||||
def encode_sequence(source: Image.Image, profile: JpegProfile, count: int = 30) -> list[EncodedImage]:
|
||||
"""Детерминированные сдвиги фотографии; это не видео реальной камеры."""
|
||||
if count < 1:
|
||||
raise ValueError("Нужна непустая последовательность")
|
||||
sequence = []
|
||||
for index in range(count):
|
||||
# Первый кадр совпадает с подготовкой Lab047. Остальные — циклический
|
||||
# сдвиг исходного фото до уменьшения; амплитуда задана, не подобрана по байтам.
|
||||
shifted = source if index == 0 else ImageChops.offset(source, (index * 7) % 61, (index * 3) % 31)
|
||||
reference = shifted.convert(profile.color_mode).resize(
|
||||
(profile.width, profile.height), Image.Resampling.LANCZOS)
|
||||
started = time.perf_counter()
|
||||
stream = BytesIO()
|
||||
reference.save(stream, format="JPEG", quality=profile.quality, subsampling=2,
|
||||
optimize=False, progressive=False)
|
||||
jpeg = stream.getvalue()
|
||||
encode_seconds = time.perf_counter() - started
|
||||
started = time.perf_counter()
|
||||
with Image.open(BytesIO(jpeg)) as image:
|
||||
image.load()
|
||||
decoded = np.asarray(image).copy()
|
||||
decode_seconds = time.perf_counter() - started
|
||||
difference = np.asarray(reference, dtype=np.float64) - decoded.astype(np.float64)
|
||||
mean_square = float(np.mean(difference * difference))
|
||||
psnr = 10 * math.log10(255**2 / mean_square) if mean_square else 999.0
|
||||
sequence.append(EncodedImage(index + 1, jpeg, encode_seconds, decode_seconds, psnr))
|
||||
return sequence
|
||||
|
||||
|
||||
def make_frame_groups(sequence: list[EncodedImage], input_fps: int) -> list[VideoFrameGroup]:
|
||||
"""Использовать существующий планировщик; LinkPacket здесь только модель времени.
|
||||
|
||||
В эфире остаются packet/fragment Lab046, без LinkPacket и без стирающего FEC.
|
||||
Все элементы планировщика имеют 256 байт, его эквивалентная скорость ниже
|
||||
выбирается так, чтобы каждый занимал ровно 5766/Rs секунд.
|
||||
"""
|
||||
if input_fps <= 0:
|
||||
raise ValueError("Частота камеры должна быть положительной")
|
||||
frames = []
|
||||
order = 0
|
||||
for index, image in enumerate(sequence):
|
||||
capture_us = round(index / input_fps * 1_000_000)
|
||||
ready_us = capture_us + math.ceil(image.encode_seconds * 1_000_000)
|
||||
fragments = transport.fragments.split_image_bytes(image.jpeg, image.image_id, FRAGMENT_BYTES)
|
||||
packets = []
|
||||
for fragment in fragments:
|
||||
payload = transport.fragments.encode_image_fragment(fragment)
|
||||
payload += bytes(256 - len(payload))
|
||||
model_packet = LinkPacket(TrafficClass.VIDEO, Direction.ROVER_TO_GROUND,
|
||||
50, order, capture_us, 0, payload)
|
||||
packets.append(AgePolicyPacket(model_packet, order, ready_us, image.image_id))
|
||||
order += 1
|
||||
frames.append(VideoFrameGroup(image.image_id, capture_us, tuple(packets), ready_us))
|
||||
return frames
|
||||
|
||||
|
||||
def simulate_sequence(sequence: list[EncodedImage], input_fps: int, symbol_rate: int,
|
||||
max_video_age: float, *, rx_seconds_per_packet: float = 0.0,
|
||||
receiver_lookahead_seconds: float = 0.0) -> dict:
|
||||
"""Очередь и идеальная доставка пакетов, не измерение эфира/камеры/экрана.
|
||||
|
||||
Измеренная стоимость RX может быть подставлена как отдельная оценка:
|
||||
последовательный обработчик пакетов, устаревший объект не допускается
|
||||
к обработке. Эта оценка не заменяет живой IQ-прогон.
|
||||
"""
|
||||
phy.PhyMode(symbol_rate)
|
||||
if not math.isfinite(rx_seconds_per_packet) or rx_seconds_per_packet < 0:
|
||||
raise ValueError("Нужна конечная неотрицательная стоимость RX")
|
||||
if not math.isfinite(receiver_lookahead_seconds) or receiver_lookahead_seconds < 0:
|
||||
raise ValueError("Нужна конечная неотрицательная задержка буфера RX")
|
||||
frames = make_frame_groups(sequence, input_fps)
|
||||
image_lookup = {image.image_id: image for image in sequence}
|
||||
equivalent_bitrate = (256 + HEADER_SIZE) * 8 * symbol_rate / FRAME_INTERVALS
|
||||
def minimum_post_rf_latency(frame: VideoFrameGroup) -> float:
|
||||
# Идеальный конвейер RF -> последовательный RX без предыдущего backlog.
|
||||
# Если уже эта нижняя оценка не укладывается в возраст, TX бесполезен.
|
||||
packet_time = FRAME_INTERVALS / symbol_rate
|
||||
return (receiver_lookahead_seconds + rx_seconds_per_packet
|
||||
+ (len(frame.packets)-1) * max(0.0, rx_seconds_per_packet-packet_time)
|
||||
+ image_lookup[frame.composite_frame_id].decode_seconds)
|
||||
schedule = schedule_video_frames(
|
||||
frames, (), FramePolicy.LATEST_ONLY, equivalent_bitrate,
|
||||
max_video_age_seconds=max_video_age,
|
||||
completion_latency_seconds=minimum_post_rf_latency,
|
||||
)
|
||||
arrivals: dict[int, list] = {}
|
||||
for sent in schedule.transmitted:
|
||||
arrivals.setdefault(sent.item.composite_frame_id, []).append(sent)
|
||||
frame_lookup = {frame.composite_frame_id: frame for frame in frames}
|
||||
horizon = len(sequence) / input_fps
|
||||
rx_cursor = 0.0
|
||||
displayed = []
|
||||
traces = []
|
||||
received = 0
|
||||
discarded_rx = 0
|
||||
for frame_id, sent in arrivals.items():
|
||||
frame = frame_lookup[frame_id]
|
||||
image = image_lookup[frame_id]
|
||||
first_tx, last_tx = sent[0].start_seconds, sent[-1].end_seconds
|
||||
# Идеальный packet-level путь действительно проверяет сериализованный
|
||||
# CRC и собирает JPEG; FEC/IQ проверяется отдельным интеграционным опытом.
|
||||
assembler = transport.ObjectAssembler()
|
||||
for item in sent:
|
||||
wire = packet_protocol.build_packet(item.item.packet.payload,
|
||||
phy.lab045.MESSAGE_TYPE, phy.lab045.SEQUENCE_NUMBER)
|
||||
parsed = packet_protocol.parse_packet(wire)
|
||||
assembler.offer(transport.recover_fragment_from_payload(parsed.payload))
|
||||
if assembler.recovered_object() != image.jpeg:
|
||||
raise RuntimeError("Идеальная сборка изменила JPEG")
|
||||
first_rx = sent[0].end_seconds
|
||||
predicted_end = rx_cursor
|
||||
for item in sent:
|
||||
predicted_end = max(predicted_end, item.end_seconds + receiver_lookahead_seconds) + rx_seconds_per_packet
|
||||
predicted_end += image.decode_seconds
|
||||
dropped = predicted_end - frame.generation_time_seconds > max_video_age + 1e-12
|
||||
if dropped:
|
||||
discarded_rx += 1
|
||||
object_complete = decode_complete = display_time = None
|
||||
else:
|
||||
received += 1
|
||||
rx_cursor = predicted_end
|
||||
decode_complete = predicted_end
|
||||
object_complete = decode_complete - image.decode_seconds
|
||||
display_time = decode_complete # Только модель мгновенного экрана.
|
||||
displayed.append((frame_id, display_time, display_time - frame.generation_time_seconds))
|
||||
traces.append({
|
||||
"image_id": frame_id, "capture_timestamp": frame.generation_time_seconds,
|
||||
"encode_complete_timestamp": frame.available_time_seconds,
|
||||
"queue_enter_timestamp": frame.available_time_seconds,
|
||||
"first_tx_timestamp": first_tx, "last_tx_timestamp": last_tx,
|
||||
"first_rx_timestamp": first_rx, "object_complete_timestamp": object_complete,
|
||||
"decode_complete_timestamp": decode_complete, "display_timestamp": display_time,
|
||||
"queue_age_seconds": first_tx - frame.available_time_seconds,
|
||||
"rf_age_seconds": last_tx - first_tx,
|
||||
"end_to_end_image_age_seconds": None if display_time is None else display_time - frame.generation_time_seconds,
|
||||
"dropped_at_rx_admission": dropped,
|
||||
})
|
||||
dropped_before = {drop.frame.composite_frame_id: drop for drop in schedule.dropped_frames}
|
||||
for frame in frames:
|
||||
if frame.composite_frame_id not in dropped_before:
|
||||
continue
|
||||
drop = dropped_before[frame.composite_frame_id]
|
||||
traces.append({"image_id": frame.composite_frame_id,
|
||||
"capture_timestamp": frame.generation_time_seconds,
|
||||
"encode_complete_timestamp": frame.available_time_seconds,
|
||||
"queue_enter_timestamp": frame.available_time_seconds,
|
||||
**{key: None for key in ("first_tx_timestamp", "last_tx_timestamp", "first_rx_timestamp",
|
||||
"object_complete_timestamp", "decode_complete_timestamp", "display_timestamp",
|
||||
"queue_age_seconds", "rf_age_seconds", "end_to_end_image_age_seconds")},
|
||||
"drop_reason": drop.reason, "drop_timestamp": drop.drop_time_seconds})
|
||||
active_received = sum(row["object_complete_timestamp"] is not None
|
||||
and row["object_complete_timestamp"] <= horizon + 1e-12 for row in traces)
|
||||
active_displayed = [item for item in displayed if item[1] <= horizon + 1e-12]
|
||||
ages = [item[2] for item in displayed]
|
||||
drops = Counter(drop.reason for drop in schedule.dropped_frames)
|
||||
return {
|
||||
"scope": "packet-level ideal RF model, measured JPEG CPU durations; display/camera not measured",
|
||||
"input_fps": input_fps, "symbol_rate": symbol_rate, "max_video_age_seconds": max_video_age,
|
||||
"input_frames": len(sequence), "transmitted_frames": len(arrivals),
|
||||
"dropped_before_tx": len(schedule.dropped_frames), "drop_reasons": dict(drops),
|
||||
"rf_delivered_objects": len(arrivals), "received_frames": received,
|
||||
"dropped_at_rx_admission": discarded_rx,
|
||||
"displayed_frames": len(displayed), "maximum_waiting_frames": schedule.maximum_waiting_frames,
|
||||
"observation_seconds": horizon, "active_received_frames": active_received,
|
||||
"active_displayed_frames": len(active_displayed),
|
||||
"raw_received_fps": active_received / horizon,
|
||||
"raw_display_fps": len(active_displayed) / horizon,
|
||||
"fresh_display_fps": len(active_displayed) / horizon,
|
||||
"steady_fresh_display_fps": ((len(displayed)-1)/(displayed[-1][1]-displayed[0][1])
|
||||
if len(displayed) > 1 else 0.0),
|
||||
"drain_displayed_frames": len(displayed) - len(active_displayed),
|
||||
"image_age_min_seconds": min(ages) if ages else None,
|
||||
"image_age_median_seconds": statistics.median(ages) if ages else None,
|
||||
"image_age_max_seconds": max(ages) if ages else None,
|
||||
"rx_cost_seconds_per_packet_modeled": rx_seconds_per_packet,
|
||||
"receiver_lookahead_seconds_modeled": receiver_lookahead_seconds,
|
||||
"elrs_control_in_video_queue": False,
|
||||
"frames": sorted(traces, key=lambda row: row["image_id"]),
|
||||
}
|
||||
|
||||
|
||||
def benchmark_receiver(sequence: list[EncodedImage], symbol_rate: int, *, noisy: bool = True) -> dict:
|
||||
"""Подать несколько JPEG частями IQ без TX-метаданных в RX, замерить CPU.
|
||||
|
||||
TX хранит лишь очередной физический кадр, RX — несколько блоков.
|
||||
Проверка SHA производится после независимой выдачи и сборки объекта.
|
||||
"""
|
||||
mode = phy.PhyMode(symbol_rate)
|
||||
receiver = StreamingReceiver(symbol_rate)
|
||||
random_generator = np.random.default_rng(50_050)
|
||||
completed = []
|
||||
current_assembler = None
|
||||
current_id = None
|
||||
last_completed_id = -1
|
||||
object_first_sample = None
|
||||
assembly_decode_seconds = 0.0
|
||||
retained_object_bytes_peak = 0
|
||||
packets_before_finish = 0
|
||||
emitted_jpegs_before_finish = 0
|
||||
|
||||
def consume(packets):
|
||||
nonlocal current_assembler, current_id, last_completed_id, object_first_sample
|
||||
nonlocal assembly_decode_seconds, retained_object_bytes_peak
|
||||
started = time.perf_counter()
|
||||
for packet in packets:
|
||||
fragment = transport.recover_fragment_from_payload(packet.payload)
|
||||
if fragment is None or fragment.original_size > 65536 or fragment.fragment_count > 269:
|
||||
continue
|
||||
# Для этой конечной сессии ID возрастают, без wrap/restart. Строгий
|
||||
# transport assembler защищает согласованность индексов/дубликатов.
|
||||
if fragment.image_id <= last_completed_id or (current_id is not None and fragment.image_id < current_id):
|
||||
continue
|
||||
if current_id != fragment.image_id:
|
||||
current_id = fragment.image_id
|
||||
current_assembler = transport.ObjectAssembler()
|
||||
object_first_sample = packet.start_sample
|
||||
current_assembler.offer(fragment)
|
||||
retained_object_bytes_peak = max(retained_object_bytes_peak, fragment.original_size)
|
||||
recovered = current_assembler.recovered_object()
|
||||
if recovered is not None:
|
||||
with Image.open(BytesIO(recovered)) as image:
|
||||
image.load()
|
||||
dimensions = image.size
|
||||
completed.append({"image_id": current_id, "sha256": hashlib.sha256(recovered).hexdigest(),
|
||||
"bytes": len(recovered), "dimensions": dimensions,
|
||||
"first_sample": object_first_sample, "last_sample": packet.end_sample,
|
||||
"emitted_after_sample": packet.emitted_after_sample})
|
||||
last_completed_id = current_id
|
||||
current_id = current_assembler = None
|
||||
assembly_decode_seconds += time.perf_counter() - started
|
||||
|
||||
def push_array(values):
|
||||
for offset in range(0, values.size, receiver.block_samples):
|
||||
consume(receiver.push_iq_block(values[offset:offset + receiver.block_samples]))
|
||||
|
||||
# Ненулевые несогласованные с символом интервалы, неизвестные RX.
|
||||
push_array((random_generator.normal(0, 0.2, 4321) + 1j * random_generator.normal(0, 0.2, 4321)).astype(np.complex64))
|
||||
transmitted_packets = 0
|
||||
air_samples = 0
|
||||
for image in sequence:
|
||||
for frame in transport.build_object_frames(image.jpeg, image.image_id):
|
||||
cs8 = phy.build_parameterized_tx_cs8(frame.plan, mode)
|
||||
components = np.frombuffer(cs8, dtype=np.int8).astype(np.float32)
|
||||
values = components[0::2] + 1j * components[1::2]
|
||||
air_samples += values.size
|
||||
if noisy:
|
||||
indexes = np.arange(values.size, dtype=np.float64)
|
||||
values = values * np.exp(1j * (0.73 + 2*np.pi*10*indexes/mode.sample_rate_hz)) * (-1 if transmitted_packets % 2 else 1)
|
||||
energy = float(np.sum(abs(values)**2)) / frame.plan.frame_symbol_count
|
||||
sigma = math.sqrt(energy / (2 * 10**(7/10)))
|
||||
values += sigma * (random_generator.normal(size=values.size) + 1j * random_generator.normal(size=values.size))
|
||||
push_array(np.asarray(values, np.complex64))
|
||||
transmitted_packets += 1
|
||||
gap = 211 + (transmitted_packets * 137) % 1000
|
||||
push_array((random_generator.normal(0, 0.2, gap) + 1j * random_generator.normal(0, 0.2, gap)).astype(np.complex64))
|
||||
# Хвост — ограниченный поток фона, не вся запись. Позволяет проверить
|
||||
# выдачу последнего JPEG именно из push, до finish.
|
||||
left = receiver.lookahead + receiver.core_samples
|
||||
while left:
|
||||
count = min(left, receiver.block_samples)
|
||||
push_array((random_generator.normal(0, 0.2, count) + 1j * random_generator.normal(0, 0.2, count)).astype(np.complex64))
|
||||
left -= count
|
||||
packets_before_finish = receiver.packet_count
|
||||
emitted_jpegs_before_finish = len(completed)
|
||||
consume(receiver.finish())
|
||||
expected = {image.image_id: hashlib.sha256(image.jpeg).hexdigest() for image in sequence}
|
||||
exact = {item["image_id"]: item["sha256"] for item in completed} == expected
|
||||
processing = receiver.processing_seconds + assembly_decode_seconds
|
||||
capture_seconds = receiver.total_samples / mode.sample_rate_hz
|
||||
return {
|
||||
"symbol_rate": symbol_rate, "samples_per_symbol": mode.samples_per_symbol,
|
||||
"detection_decimation": receiver.decimation, "input_images": len(sequence),
|
||||
"tx_rf_packets": transmitted_packets, "accepted_rf_packets": receiver.packet_count,
|
||||
"completed_images": len(completed), "all_sha_match": exact,
|
||||
"packets_before_finish": packets_before_finish, "images_before_finish": emitted_jpegs_before_finish,
|
||||
"capture_seconds": capture_seconds, "payload_airtime_seconds": air_samples / mode.sample_rate_hz,
|
||||
"processing_seconds": processing, "processing_per_rf_second": processing / capture_seconds,
|
||||
"processing_per_payload_airtime_second": processing / (air_samples / mode.sample_rate_hz),
|
||||
"coarse_seconds": receiver.coarse_seconds, "strict_seconds": receiver.strict_seconds,
|
||||
"assembly_jpeg_decode_seconds": assembly_decode_seconds,
|
||||
"processing_seconds_per_rf_packet": processing / transmitted_packets,
|
||||
"peak_iq_buffer_samples": receiver.peak_buffer_samples,
|
||||
"iq_buffer_limit_samples": receiver.buffer_limit_samples,
|
||||
"working_arrays_estimate_bytes": receiver.memory_estimate_bytes + 2 * 65536,
|
||||
"memory_is_process_rss_measurement": False,
|
||||
"peak_object_payload_bytes": retained_object_bytes_peak,
|
||||
"candidate_count": receiver.candidate_count, "peak_candidates_in_block": receiver.peak_candidate_count,
|
||||
"rejected_reasons": dict(receiver.rejected_reasons),
|
||||
"buffer_release_delay_upper_seconds": (receiver.lookahead + 2*receiver.core_samples) / mode.sample_rate_hz,
|
||||
"noisy": noisy, "coded_bit_ebn0_db": 7 if noisy else None, "cfo_hz": 10 if noisy else 0,
|
||||
"candidate_threshold": phy.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD,
|
||||
"completed": completed, "rf_tx": 0,
|
||||
}
|
||||
|
||||
|
||||
def run_functional_tests() -> None:
|
||||
assert maximum_jpeg_bytes(500_000, 5) == 4148
|
||||
assert maximum_jpeg_bytes(1_000_000, 10) == 4148
|
||||
assert fragment_count(244) == 1 and fragment_count(245) == 2
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--reuse-benchmarks", action="store_true", help="Переиспользовать текущие локальные замеры IQ")
|
||||
args = parser.parse_args()
|
||||
run_functional_tests()
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
source_path = ROOT / "data/raw/lab011_scene.jpg"
|
||||
with Image.open(source_path) as source:
|
||||
source.load()
|
||||
sequences = {profile.name: encode_sequence(source, profile) for profile in PROFILES}
|
||||
control = sequences["A"][0].jpeg
|
||||
if hashlib.sha256(control).hexdigest() != phy.EXPECTED_JPEG_SHA256:
|
||||
raise RuntimeError("Профиль A не воспроизвёл контрольный JPEG Lab047")
|
||||
summaries = []
|
||||
for profile in PROFILES:
|
||||
sequence = sequences[profile.name]
|
||||
(OUTPUT / f"profile_{profile.name}.jpg").write_bytes(sequence[0].jpeg)
|
||||
summaries.append({**asdict(profile), "first_jpeg_bytes": len(sequence[0].jpeg),
|
||||
"min_jpeg_bytes": min(len(item.jpeg) for item in sequence),
|
||||
"max_jpeg_bytes": max(len(item.jpeg) for item in sequence),
|
||||
"first_psnr_db": sequence[0].psnr_db,
|
||||
"median_encode_seconds": statistics.median(item.encode_seconds for item in sequence),
|
||||
"median_decode_seconds": statistics.median(item.decode_seconds for item in sequence),
|
||||
"sha256_first": hashlib.sha256(sequence[0].jpeg).hexdigest(),
|
||||
"airtime_capacity": {str(rate): rate/(fragment_count(len(sequence[0].jpeg))*FRAME_INTERVALS) for rate in SYMBOL_RATES}})
|
||||
print(json.dumps(summaries, ensure_ascii=False), flush=True)
|
||||
benchmark_path = OUTPUT / "receiver_benchmarks.json"
|
||||
if args.reuse_benchmarks:
|
||||
benchmarks = json.loads(benchmark_path.read_text(encoding="utf-8"))
|
||||
expected_hashes = {image.image_id: hashlib.sha256(image.jpeg).hexdigest() for image in sequences["C"][:3]}
|
||||
if [row["symbol_rate"] for row in benchmarks] != list(SYMBOL_RATES):
|
||||
raise ValueError("Сохранены замеры других скоростей")
|
||||
for row in benchmarks:
|
||||
if (row["input_images"] != 3 or row["coded_bit_ebn0_db"] != 7
|
||||
or row["candidate_threshold"] != phy.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD
|
||||
or any(expected_hashes.get(item["image_id"]) != item["sha256"] for item in row["completed"])):
|
||||
raise ValueError("Сохранённые замеры относятся к другому сценарию")
|
||||
else:
|
||||
benchmarks = []
|
||||
for rate in SYMBOL_RATES:
|
||||
print(f"IQ benchmark {rate}: 3 JPEG profile C", flush=True)
|
||||
result = benchmark_receiver(sequences["C"][:3], rate)
|
||||
benchmarks.append(result)
|
||||
print(json.dumps({key: result[key] for key in ("symbol_rate", "completed_images", "all_sha_match", "processing_per_rf_second", "working_arrays_estimate_bytes")}), flush=True)
|
||||
benchmark_path.write_text(json.dumps(benchmarks, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
matrix = []
|
||||
receiver_estimates = []
|
||||
for profile in PROFILES:
|
||||
for fps in INPUT_RATES:
|
||||
for rate in SYMBOL_RATES:
|
||||
for age in AGE_LIMITS:
|
||||
row = simulate_sequence(sequences[profile.name], fps, rate, age)
|
||||
matrix.append({"profile": profile.name, **row})
|
||||
for measured in benchmarks:
|
||||
if not measured["all_sha_match"]:
|
||||
continue
|
||||
for age in AGE_LIMITS:
|
||||
receiver_estimates.append({"profile": "C", **simulate_sequence(sequences["C"], 5,
|
||||
measured["symbol_rate"], age, rx_seconds_per_packet=measured["processing_seconds_per_rf_packet"],
|
||||
receiver_lookahead_seconds=measured["buffer_release_delay_upper_seconds"])})
|
||||
result = {
|
||||
"stage": "Lab050 software video stream", "lab049_commit": "cdf5396e807b83f82a264632dc46f71428fcd616",
|
||||
"source_sha256": hashlib.sha256(source_path.read_bytes()).hexdigest(),
|
||||
"sequence": "30 deterministic cyclic shifts of one project photo, not camera video",
|
||||
"profiles": summaries,
|
||||
"maximum_jpeg_bytes": {str(rate): {str(fps): maximum_jpeg_bytes(rate, fps) for fps in INPUT_RATES} for rate in SYMBOL_RATES},
|
||||
"receiver_benchmarks": benchmarks, "packet_model_matrix": matrix,
|
||||
"measured_rx_cost_model": receiver_estimates,
|
||||
"conditional_accelerated_models": [
|
||||
{"profile": "C", "is_current_receiver_performance": False,
|
||||
"required_speedup_vs_measured_cost": next(row["processing_seconds_per_rf_packet"]
|
||||
for row in benchmarks if row["symbol_rate"] == 500000) / cost,
|
||||
**simulate_sequence(sequences["C"], fps, 500000, .2, rx_seconds_per_packet=cost,
|
||||
receiver_lookahead_seconds=next(row["buffer_release_delay_upper_seconds"]
|
||||
for row in benchmarks if row["symbol_rate"] == 500000))}
|
||||
for fps, cost in ((5, .016), (10, .012))
|
||||
],
|
||||
"modeled_unknown_latencies": {"capture": "unknown, modeled zero", "display": "unknown, modeled zero",
|
||||
"encode": "JPEG encode CPU measured; source preparation outside timing",
|
||||
"decode": "JPEG decode CPU measured", "rf": "calculated waveform airtime"},
|
||||
"elrs_control_path": "SEPARATE", "rf_tx": 0,
|
||||
}
|
||||
(OUTPUT / "result.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
selected = next(row for row in matrix if row["profile"] == "C" and row["input_fps"] == 5 and row["symbol_rate"] == 500000 and row["max_video_age_seconds"] == .2)
|
||||
print(json.dumps({key: value for key, value in selected.items() if key != "frames"}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
320
experiments/lab051_receiver_performance.py
Normal file
320
experiments/lab051_receiver_performance.py
Normal file
@@ -0,0 +1,320 @@
|
||||
"""Lab051: измеренный блочный RX и возраст JPEG в шестисекундном потоке.
|
||||
|
||||
Источник синтетический, из реального фото. RF не выполняется. Виртуальное
|
||||
время работника использует фактическое время каждого push/сборки/JPEG decode.
|
||||
Это офлайн-воспроизведение; отставание явно означает требуемый внешний IQ backlog.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from dataclasses import asdict
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
|
||||
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 lab050_video_stream as video
|
||||
from experiments import lab051_streaming_receiver as fast
|
||||
from experiments.lab051_receiver_profile import OUTPUT, ROOT, StageProfiler, array_observation, process_memory
|
||||
from protocol.video_frame_scheduler import FramePolicy, schedule_video_frames
|
||||
|
||||
|
||||
def write_result(name: str, result: dict) -> None:
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
(OUTPUT / name).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
class ImageSink:
|
||||
"""Только текущий объект; никаких ожидаемых ID, размеров JPEG или TX bytes."""
|
||||
|
||||
def __init__(self):
|
||||
self.object_id = -1
|
||||
self.last_completed_id = -1
|
||||
self.assembler = None
|
||||
self.first_rf_sample = None
|
||||
self.first_rx_time = None
|
||||
self.retained_payload_peak = 0
|
||||
self.assembly_seconds = self.jpeg_decode_seconds = 0.0
|
||||
self.rejected = Counter()
|
||||
|
||||
def offer(self, packet, received_time: float) -> dict | None:
|
||||
started = time.perf_counter()
|
||||
fragment = video.transport.recover_fragment_from_payload(packet.payload)
|
||||
if fragment is None or fragment.original_size > 65536 or fragment.fragment_count > 269:
|
||||
self.rejected["invalid_fragment"] += 1
|
||||
self.assembly_seconds += time.perf_counter() - started
|
||||
return None
|
||||
if fragment.image_id < self.object_id or fragment.image_id <= self.last_completed_id:
|
||||
self.rejected["older_object"] += 1
|
||||
self.assembly_seconds += time.perf_counter() - started
|
||||
return None
|
||||
if fragment.image_id > self.object_id:
|
||||
self.object_id = fragment.image_id
|
||||
self.assembler = video.transport.ObjectAssembler()
|
||||
self.first_rf_sample = packet.start_sample
|
||||
self.first_rx_time = received_time
|
||||
self.assembler.offer(fragment)
|
||||
self.retained_payload_peak = max(self.retained_payload_peak, fragment.original_size)
|
||||
jpeg = self.assembler.recovered_object()
|
||||
self.assembly_seconds += time.perf_counter() - started
|
||||
if jpeg is None:
|
||||
return None
|
||||
decode_started = time.perf_counter()
|
||||
try:
|
||||
with Image.open(BytesIO(jpeg)) as image:
|
||||
image.load()
|
||||
dimensions = image.size
|
||||
except (OSError, ValueError):
|
||||
self.rejected["jpeg_decode"] += 1
|
||||
return None
|
||||
finally:
|
||||
self.jpeg_decode_seconds += time.perf_counter() - decode_started
|
||||
self.last_completed_id = self.object_id
|
||||
self.assembler = None
|
||||
return {"image_id": self.object_id, "jpeg": jpeg, "dimensions": dimensions,
|
||||
"first_rf_sample": self.first_rf_sample, "last_rf_sample": packet.end_sample,
|
||||
"first_rx_time": self.first_rx_time}
|
||||
|
||||
|
||||
def run_stream(sequence: list[video.EncodedImage]) -> dict:
|
||||
"""Вход RX — только последовательные IQ-блоки; TX-диагностика после JPEG."""
|
||||
mode = video.phy.PhyMode(500000)
|
||||
receiver = fast.StreamingReceiver()
|
||||
sink = ImageSink()
|
||||
random_generator = np.random.default_rng(50050)
|
||||
frames = video.make_frame_groups(sequence, 5)
|
||||
packet_time = video.FRAME_INTERVALS / mode.symbol_rate
|
||||
bitrate = (256 + video.HEADER_SIZE) * 8 / packet_time
|
||||
# TX использует известную нижнюю задержку радио/буфера, без предсказания
|
||||
# будущего CPU и без знания RX verdict. Перегрузка RX измеряется ниже.
|
||||
buffer_delay = (receiver.lookahead + 2*receiver.core_samples) / mode.sample_rate_hz
|
||||
schedule = schedule_video_frames(frames, (), FramePolicy.LATEST_ONLY, bitrate,
|
||||
max_video_age_seconds=.2, completion_latency_seconds=buffer_delay)
|
||||
starts = {}
|
||||
for sent in schedule.transmitted:
|
||||
starts.setdefault(sent.item.composite_frame_id, sent.start_seconds)
|
||||
horizon = len(sequence) / 5
|
||||
encoded_lookup = {image.image_id: image for image in sequence}
|
||||
capture_lookup = {frame.composite_frame_id: frame.generation_time_seconds for frame in frames}
|
||||
pending = np.empty(0, np.complex64)
|
||||
total_samples = tx_packets = 0
|
||||
worker_time = rx_processing = max_lag = 0.0
|
||||
completed = []
|
||||
block_metrics = []
|
||||
rss_samples = []
|
||||
|
||||
def consume_block(block, *, final=False):
|
||||
nonlocal total_samples, worker_time, rx_processing, max_lag
|
||||
total_samples += block.size
|
||||
arrival = total_samples / mode.sample_rate_hz
|
||||
worker_start = max(worker_time, arrival)
|
||||
started = time.perf_counter()
|
||||
packets = receiver.finish() if final else receiver.push_iq_block(block)
|
||||
cpu = time.perf_counter() - started
|
||||
rx_processing += cpu
|
||||
worker_time = worker_start + cpu
|
||||
for packet in packets:
|
||||
started = time.perf_counter()
|
||||
item = sink.offer(packet, worker_time)
|
||||
cost = time.perf_counter() - started
|
||||
worker_time += cost
|
||||
if item is not None:
|
||||
# Приём/сборка/JPEG уже завершены. TX используется только теперь.
|
||||
jpeg = item.pop("jpeg")
|
||||
image_id = item["image_id"]
|
||||
capture = capture_lookup.get(image_id)
|
||||
age = None if capture is None else worker_time - capture
|
||||
completed.append({**item, "decode_complete_timestamp": worker_time,
|
||||
"capture_timestamp": capture, "image_age_seconds": age,
|
||||
"fresh": age is not None and 0 <= age <= .2,
|
||||
"sha256": hashlib.sha256(jpeg).hexdigest(),
|
||||
"sha_match": image_id in encoded_lookup and jpeg == encoded_lookup[image_id].jpeg})
|
||||
lag = max(0., worker_time - arrival)
|
||||
max_lag = max(max_lag, lag)
|
||||
block_metrics.append({"samples_received": total_samples, "arrival_time": arrival,
|
||||
"worker_start": worker_start, "worker_end": worker_time,
|
||||
"rx_cpu_seconds": cpu, "packets": len(packets), "lag_seconds": lag})
|
||||
if len(block_metrics) % 64 == 0:
|
||||
rss_samples.append({"samples": total_samples, **process_memory()})
|
||||
|
||||
def push(values):
|
||||
nonlocal pending
|
||||
offset = 0
|
||||
if pending.size:
|
||||
needed = receiver.block_samples - pending.size
|
||||
joined = np.concatenate((pending, values[:needed]))
|
||||
offset = min(needed, values.size)
|
||||
if joined.size < receiver.block_samples:
|
||||
pending = joined
|
||||
return
|
||||
consume_block(joined)
|
||||
pending = np.empty(0, np.complex64)
|
||||
while offset + receiver.block_samples <= values.size:
|
||||
consume_block(values[offset:offset + receiver.block_samples])
|
||||
offset += receiver.block_samples
|
||||
pending = values[offset:].copy()
|
||||
|
||||
def background(count):
|
||||
while count:
|
||||
amount = min(count, receiver.block_samples)
|
||||
values = .2 * (random_generator.normal(size=amount) + 1j*random_generator.normal(size=amount))
|
||||
push(values.astype(np.complex64))
|
||||
count -= amount
|
||||
|
||||
for image in sequence:
|
||||
if image.image_id not in starts:
|
||||
continue
|
||||
target = math.ceil(starts[image.image_id] * mode.sample_rate_hz)
|
||||
background(max(0, target - total_samples - pending.size))
|
||||
for frame in video.transport.build_object_frames(image.jpeg, image.image_id):
|
||||
raw = video.phy.build_parameterized_tx_cs8(frame.plan, mode)
|
||||
components = np.frombuffer(raw, np.int8).astype(np.float32)
|
||||
values = components[0::2] + 1j*components[1::2]
|
||||
indexes = np.arange(values.size)
|
||||
values = values * np.exp(1j*(.73 + 2*np.pi*10*indexes/mode.sample_rate_hz)) * (-1 if tx_packets % 2 else 1)
|
||||
energy = float(np.sum(abs(values)**2)) / frame.plan.frame_symbol_count
|
||||
sigma = math.sqrt(energy / (2*10**(.7)))
|
||||
values += sigma*(random_generator.normal(size=values.size) + 1j*random_generator.normal(size=values.size))
|
||||
push(values.astype(np.complex64))
|
||||
tx_packets += 1
|
||||
background(max(0, math.ceil(horizon * mode.sample_rate_hz) + receiver.lookahead + receiver.core_samples
|
||||
- total_samples - pending.size))
|
||||
if pending.size:
|
||||
consume_block(pending)
|
||||
consume_block(np.empty(0,np.complex64), final=True)
|
||||
fresh = [row for row in completed if row["fresh"]]
|
||||
displayed_in_window = [row for row in fresh if row["decode_complete_timestamp"] <= horizon]
|
||||
decoded_in_window = [row for row in completed if row["decode_complete_timestamp"] <= horizon]
|
||||
ages = [row["image_age_seconds"] for row in completed if row["image_age_seconds"] is not None]
|
||||
fresh_ages = [row["image_age_seconds"] for row in fresh]
|
||||
processing = rx_processing + sink.assembly_seconds + sink.jpeg_decode_seconds
|
||||
return {
|
||||
"scope": "continuous synthetic IQ; measured per-block CPU event replay; camera/display latency UNKNOWN",
|
||||
"symbol_rate":500000,"samples_per_symbol":16,"input_fps":5,"input_frames":len(sequence),
|
||||
"observation_seconds":horizon,"iq_seconds":total_samples/mode.sample_rate_hz,
|
||||
"tx_images":len(starts),"tx_physical_packets":tx_packets,
|
||||
"dropped_before_tx":len(schedule.dropped_frames),"maximum_waiting_frames":schedule.maximum_waiting_frames,
|
||||
"rx_physical_packets":receiver.packet_count,"received_images":len(completed),
|
||||
"object_jpeg_integrity":len(completed)==len(starts) and all(row["sha_match"] for row in completed),
|
||||
"dropped_stale_at_display":len(completed)-len(fresh),"displayed":len(fresh),"fresh_displayed":len(fresh),
|
||||
"raw_received_fps":len(decoded_in_window)/horizon,
|
||||
"raw_display_fps":len(displayed_in_window)/horizon,"fresh_display_fps":len(displayed_in_window)/horizon,
|
||||
"drain_fresh_displayed":len(fresh)-len(displayed_in_window),
|
||||
"image_age_min_seconds":min(ages) if ages else None,
|
||||
"image_age_median_seconds":statistics.median(ages) if ages else None,
|
||||
"image_age_max_seconds":max(ages) if ages else None,
|
||||
"fresh_image_age_seconds":fresh_ages,"max_video_age_seconds":.2,
|
||||
"processing_seconds":processing,"rx_processing_seconds":rx_processing,
|
||||
"assembly_seconds":sink.assembly_seconds,"jpeg_decode_seconds":sink.jpeg_decode_seconds,
|
||||
"processing_per_rf_second":processing/(total_samples/mode.sample_rate_hz),
|
||||
"physical_packet_processing_seconds":processing/tx_packets if tx_packets else None,
|
||||
"maximum_cpu_lag_seconds":max_lag,
|
||||
"implied_external_iq_backlog_bytes":math.ceil(max_lag*mode.sample_rate_hz)*8,
|
||||
"live_external_queue_implemented":False,
|
||||
"retained_iq_peak_bytes":receiver.peak_buffer_samples*8,
|
||||
"working_arrays_upper_estimate_bytes":receiver.memory_estimate_bytes + 20*receiver.buffer_limit_samples*8,
|
||||
"object_payload_peak_bytes":sink.retained_payload_peak,"process_memory":process_memory(),
|
||||
"rss_samples":rss_samples,"candidate_count":receiver.candidate_count,
|
||||
"rejected_reasons":dict(receiver.rejected_reasons),"front_statistics":dict(receiver.front_statistics),
|
||||
"decimated_samples_reused":receiver.decimator.reused_samples,
|
||||
"actual_decimator_input_samples":receiver.decimator.filtered_input_samples,
|
||||
"images":completed,"blocks":block_metrics,"rf_tx":0,
|
||||
}
|
||||
|
||||
|
||||
def run_memory_test(block_count: int = 4096) -> dict:
|
||||
"""33,55 с без истории IQ/пакетов; нулевой фон отделён от noisy IQ опыта."""
|
||||
receiver = fast.StreamingReceiver()
|
||||
values = np.zeros(receiver.block_samples,np.complex64)
|
||||
checkpoints=[]
|
||||
for index in range(block_count):
|
||||
assert receiver.push_iq_block(values) == []
|
||||
assert receiver.buffer.size <= receiver.buffer_limit_samples
|
||||
if (index+1) % 512 == 0:
|
||||
checkpoints.append({"block":index+1,"iq_bytes":receiver.buffer.nbytes,
|
||||
"decimated_bytes":receiver.decimator.values.nbytes,**process_memory()})
|
||||
assert receiver.finish()==[]
|
||||
return {"blocks":block_count,"model_seconds":block_count*values.size/8000000,
|
||||
"background":"zeros; noisy background covered by six-second integration",
|
||||
"peak_iq_bytes":receiver.peak_buffer_samples*8,"iq_limit_bytes":receiver.buffer_limit_samples*8,
|
||||
"retained_after_finish":receiver.buffer.nbytes+receiver.decimator.values.nbytes,
|
||||
"checkpoints":checkpoints,"rf_tx":0}
|
||||
|
||||
|
||||
def run_candidate_replay() -> dict:
|
||||
metadata=json.loads((OUTPUT/"optimized_examples.json").read_text(encoding="utf-8"))
|
||||
result={}
|
||||
for kind, entry in metadata.items():
|
||||
raw=np.load(OUTPUT/f"optimized_{kind}_window.npy")
|
||||
candidate=fast.offline.CoarseCandidate(entry["offset"],entry["score"])
|
||||
def call(optimized=False):
|
||||
with fast.previous.streaming_mode(video.phy.PhyMode(500000)):
|
||||
def decode():
|
||||
return fast.offline.decode_candidate_from_memmap(raw,entry["sample_count"],candidate,1,
|
||||
read_iq_window=lambda data,start,end:data[2*start:2*end].view(np.complex64))
|
||||
if not optimized:
|
||||
return decode()
|
||||
front=fast.BlockFrontEnd(raw.view(np.complex64),65536,Counter())
|
||||
with fast.optimized_context(front): return decode()
|
||||
old=call(False)
|
||||
new=call(True)
|
||||
assert (old.accepted,old.reason,old.refined_start_sample)==(new.accepted,new.reason,new.refined_start_sample)
|
||||
result[kind]={"reason":new.reason,"baseline_arrays":array_observation(lambda:call(False)),
|
||||
"optimized_arrays":array_observation(lambda:call(True))}
|
||||
return result
|
||||
|
||||
|
||||
def run_benchmark(sequence) -> dict:
|
||||
original=video.StreamingReceiver
|
||||
try:
|
||||
video.StreamingReceiver=fast.StreamingReceiver
|
||||
repeats=[video.benchmark_receiver(sequence[:3],500000) for unused in range(3)]
|
||||
profiler=StageProfiler()
|
||||
with profiler.measure(optimized=True): profiled=video.benchmark_receiver(sequence[:3],500000)
|
||||
metadata={}
|
||||
for kind,(raw,count,candidate) in profiler.examples.items():
|
||||
np.save(OUTPUT/f"optimized_{kind}_window.npy",raw)
|
||||
metadata[kind]={"sample_count":count,"offset":candidate.sample_offset,"score":candidate.score}
|
||||
write_result("optimized_examples.json",metadata)
|
||||
return {"repeats":repeats,"median_processing_seconds":statistics.median(row["processing_seconds"] for row in repeats),
|
||||
"profile":profiler.summary(),"profiled_benchmark":profiled,"rf_tx":0}
|
||||
finally:
|
||||
video.StreamingReceiver=original
|
||||
|
||||
|
||||
def run_functional_tests() -> None:
|
||||
assert video.maximum_jpeg_bytes(500000,5)==4148
|
||||
assert fast.lab045.conv.CONSTRAINT_LENGTH==7
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--stage",choices=("benchmark","stream","memory","replay","all"),default="all")
|
||||
args=parser.parse_args()
|
||||
OUTPUT.mkdir(parents=True,exist_ok=True)
|
||||
with Image.open(ROOT/"data/raw/lab011_scene.jpg") as source:
|
||||
sequence=video.encode_sequence(source,video.PROFILES[2],30)
|
||||
for name,function in (("benchmark",lambda:run_benchmark(sequence)),("stream",lambda:run_stream(sequence)),
|
||||
("memory",run_memory_test),("replay",run_candidate_replay)):
|
||||
if args.stage not in ("all",name): continue
|
||||
print(f"Lab051 {name}: START; RF TX=0",flush=True)
|
||||
result=function()
|
||||
write_result(f"{name}.json",result)
|
||||
print(json.dumps({key:value for key,value in result.items() if key not in (
|
||||
"blocks","images","repeats","profiled_benchmark","rss_samples")},ensure_ascii=False),flush=True)
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
246
experiments/lab051_receiver_profile.py
Normal file
246
experiments/lab051_receiver_profile.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Lab051: воспроизводимое профилирование приёмника, без оборудования.
|
||||
|
||||
Измерение времени отделено от медленного наблюдения массивов/tracemalloc.
|
||||
Классификация valid/false происходит только после штатного PHY/FEC/CRC.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import asdict, dataclass
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
import json
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import tracemalloc
|
||||
import weakref
|
||||
|
||||
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 lab046c_offline_recovery as offline
|
||||
from experiments import lab050_video_stream as video
|
||||
from experiments.lab050_streaming_receiver import streaming_mode
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "data/processed/lab051"
|
||||
lab045 = video.phy.lab045
|
||||
lab043 = lab045.lab044.lab043
|
||||
|
||||
|
||||
def process_memory() -> dict:
|
||||
"""Working set Windows без psutil; чужие процессы не перечисляются."""
|
||||
if sys.platform != "win32":
|
||||
return {"rss_bytes": None, "peak_rss_bytes": None}
|
||||
|
||||
class Counters(ctypes.Structure):
|
||||
_fields_ = [("cb", wintypes.DWORD), ("PageFaultCount", wintypes.DWORD)] + [
|
||||
(name, ctypes.c_size_t) for name in (
|
||||
"PeakWorkingSetSize", "WorkingSetSize", "QuotaPeakPagedPoolUsage",
|
||||
"QuotaPagedPoolUsage", "QuotaPeakNonPagedPoolUsage", "QuotaNonPagedPoolUsage",
|
||||
"PagefileUsage", "PeakPagefileUsage")]
|
||||
|
||||
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
psapi = ctypes.WinDLL("psapi", use_last_error=True)
|
||||
kernel.GetCurrentProcess.restype = wintypes.HANDLE
|
||||
psapi.GetProcessMemoryInfo.argtypes = [wintypes.HANDLE, ctypes.POINTER(Counters), wintypes.DWORD]
|
||||
counters = Counters()
|
||||
counters.cb = ctypes.sizeof(counters)
|
||||
if not psapi.GetProcessMemoryInfo(kernel.GetCurrentProcess(), ctypes.byref(counters), counters.cb):
|
||||
return {"rss_bytes": None, "peak_rss_bytes": None, "error": ctypes.get_last_error()}
|
||||
return {"rss_bytes": counters.WorkingSetSize, "peak_rss_bytes": counters.PeakWorkingSetSize}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stage:
|
||||
calls: int = 0
|
||||
seconds: float = 0.0
|
||||
exclusive_seconds: float = 0.0
|
||||
|
||||
|
||||
class StageProfiler:
|
||||
"""Только последовательное профилирование; подмены возвращаются в finally."""
|
||||
|
||||
def __init__(self):
|
||||
self.rows = []
|
||||
self.current = None
|
||||
self.shared = defaultdict(Stage)
|
||||
self.stack = []
|
||||
self.examples = {}
|
||||
|
||||
@contextmanager
|
||||
def measure(self, *, optimized: bool = False):
|
||||
patches = []
|
||||
targets = (
|
||||
(offline, "_correlation_scores", "coarse_localization"),
|
||||
(lab043, "_refine_frame_start", "refinement"),
|
||||
(lab043, "_matched_waveform", "matched_filter"),
|
||||
(lab043, "_preamble_timing_offset", "timing"),
|
||||
(lab043, "_timing_offsets", "timing_fallback"),
|
||||
(lab043, "_estimate_cfo_gain", "cfo_gain"),
|
||||
(lab043, "_evaluate_symbols", "phase_diagnostics"),
|
||||
(lab045, "decode_phy_header_soft", "phy_header"),
|
||||
(lab045, "analyze_post_preamble_soft", "body_pipeline"),
|
||||
(lab045.conv, "viterbi_decode_soft", "viterbi"),
|
||||
(lab045, "parse_packet", "crc_parser"),
|
||||
)
|
||||
if optimized:
|
||||
from experiments import lab051_streaming_receiver as fast
|
||||
targets += ((fast.BlockFrontEnd, "refine", "refinement"),
|
||||
(fast.BlockFrontEnd, "coarse_scores", "coarse_localization"),
|
||||
(fast.BlockFrontEnd, "matched_window", "matched_filter"),
|
||||
(fast.BlockFrontEnd, "timing", "timing"),
|
||||
(fast, "fast_viterbi", "viterbi"))
|
||||
|
||||
def wrap(function, name):
|
||||
def measured(*args, **kwargs):
|
||||
scope = self.shared if self.current is None else self.current
|
||||
stage = scope[name]
|
||||
started = time.perf_counter()
|
||||
entry = [0.0]
|
||||
self.stack.append(entry)
|
||||
try:
|
||||
return function(*args, **kwargs)
|
||||
finally:
|
||||
elapsed = time.perf_counter() - started
|
||||
self.stack.pop()
|
||||
if self.stack:
|
||||
self.stack[-1][0] += elapsed
|
||||
stage.calls += 1
|
||||
stage.seconds += elapsed
|
||||
stage.exclusive_seconds += elapsed - entry[0]
|
||||
return measured
|
||||
|
||||
for owner, name, stage in targets:
|
||||
original = getattr(owner, name)
|
||||
patches.append((owner, name, original))
|
||||
setattr(owner, name, wrap(original, stage))
|
||||
original_decode = offline.decode_candidate_from_memmap
|
||||
|
||||
def candidate(raw, sample_count, coarse, number, **kwargs):
|
||||
self.current = defaultdict(Stage)
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
outcome = original_decode(raw, sample_count, coarse, number, **kwargs)
|
||||
elapsed = time.perf_counter() - started
|
||||
kind = "valid" if outcome.accepted else "false"
|
||||
self.rows.append({"kind": kind, "seconds": elapsed, "reason": outcome.reason,
|
||||
"offset": coarse.sample_offset, "refined": outcome.refined_start_sample,
|
||||
"stages": {name: asdict(row) for name, row in self.current.items()}})
|
||||
# По одному примеру после решения, только для отдельного replay.
|
||||
if kind not in self.examples and (kind == "valid" or outcome.reason.startswith("PHY_HEADER_MAGIC")):
|
||||
self.examples[kind] = (raw.copy(), sample_count, coarse)
|
||||
return outcome
|
||||
finally:
|
||||
self.current = None
|
||||
|
||||
offline.decode_candidate_from_memmap = candidate
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
offline.decode_candidate_from_memmap = original_decode
|
||||
for owner, name, original in reversed(patches):
|
||||
setattr(owner, name, original)
|
||||
|
||||
def summary(self) -> dict:
|
||||
result = {"shared_stages": {name: asdict(row) for name, row in self.shared.items()}}
|
||||
for kind in ("valid", "false"):
|
||||
selected = [row for row in self.rows if row["kind"] == kind]
|
||||
stages = defaultdict(Stage)
|
||||
for row in selected:
|
||||
for name, stage in row["stages"].items():
|
||||
stages[name].calls += stage["calls"]
|
||||
stages[name].seconds += stage["seconds"]
|
||||
stages[name].exclusive_seconds += stage["exclusive_seconds"]
|
||||
times = [row["seconds"] for row in selected]
|
||||
result[kind] = {"count": len(times), "mean_seconds": statistics.mean(times) if times else None,
|
||||
"median_seconds": statistics.median(times) if times else None,
|
||||
"total_seconds": sum(times),
|
||||
"stages": {name: asdict(row) for name, row in stages.items()}}
|
||||
return result
|
||||
|
||||
|
||||
def array_observation(function) -> dict:
|
||||
"""Нижняя граница числа массивов в локальных переменных проектного Python.
|
||||
|
||||
Временные выражения и внутренности C/SciPy не видны line tracer-у.
|
||||
tracemalloc отдельно считает пик отслеживаемой памяти, включая NumPy.
|
||||
Эти проходы НИКОГДА не используются как измерение производительности.
|
||||
"""
|
||||
seen = {}
|
||||
counters = {"observed_array_objects": 0, "observed_owned_arrays": 0, "observed_owned_bytes_total": 0}
|
||||
|
||||
def trace(frame, event, argument):
|
||||
filename = frame.f_code.co_filename.replace("\\", "/")
|
||||
if not ("/experiments/" in filename or "/protocol/" in filename):
|
||||
return None
|
||||
if event in ("line", "return"):
|
||||
for value in frame.f_locals.values():
|
||||
if not isinstance(value, np.ndarray):
|
||||
continue
|
||||
identity = id(value)
|
||||
if identity in seen and seen[identity]() is value:
|
||||
continue
|
||||
seen[identity] = weakref.ref(value)
|
||||
counters["observed_array_objects"] += 1
|
||||
if value.flags.owndata:
|
||||
counters["observed_owned_arrays"] += 1
|
||||
counters["observed_owned_bytes_total"] += value.nbytes
|
||||
return trace
|
||||
|
||||
previous = sys.gettrace()
|
||||
try:
|
||||
sys.settrace(trace)
|
||||
function()
|
||||
finally:
|
||||
sys.settrace(previous)
|
||||
tracemalloc.start()
|
||||
try:
|
||||
function()
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
finally:
|
||||
tracemalloc.stop()
|
||||
return {**counters, "tracemalloc_retained_bytes": current, "tracemalloc_peak_bytes": peak,
|
||||
"array_count_is_lower_bound": True}
|
||||
|
||||
|
||||
def run_functional_tests() -> None:
|
||||
assert video.phy.PhyMode(500_000).samples_per_symbol == 16
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
with Image.open(ROOT / "data/raw/lab011_scene.jpg") as source:
|
||||
sequence = video.encode_sequence(source, video.PROFILES[2], 3)
|
||||
profiler = StageProfiler()
|
||||
with profiler.measure():
|
||||
benchmark = video.benchmark_receiver(sequence, 500_000)
|
||||
result = {"benchmark": benchmark, "profile": profiler.summary(), "rf_tx": 0}
|
||||
(OUTPUT / "baseline_profile.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(result["profile"], indent=2), flush=True)
|
||||
# Небольшие ограниченные окна, не полная запись. В Git не включать.
|
||||
metadata = {}
|
||||
for kind, (raw, count, coarse) in profiler.examples.items():
|
||||
np.save(OUTPUT / f"baseline_{kind}_window.npy", raw)
|
||||
metadata[kind] = {"sample_count": count, "offset": coarse.sample_offset, "score": coarse.score}
|
||||
with streaming_mode(video.phy.PhyMode(500_000)):
|
||||
call = lambda: offline.decode_candidate_from_memmap(
|
||||
raw, count, coarse, 1,
|
||||
read_iq_window=lambda data, start, end: data[2*start:2*end].view(np.complex64))
|
||||
result.setdefault("array_observation", {})[kind] = array_observation(call)
|
||||
(OUTPUT / "baseline_examples.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
|
||||
result["process_memory_after_profiling"] = process_memory()
|
||||
(OUTPUT / "baseline_profile.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(result["array_observation"]), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
364
experiments/lab051_streaming_receiver.py
Normal file
364
experiments/lab051_streaming_receiver.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""Lab051: общий front-end и эквивалентный timing на 500 ксимв/с.
|
||||
|
||||
PHY, FEC, CRC и порог первого каскада заимствованы без изменения правил.
|
||||
Подмены старых функций действуют только внутри последовательного push.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
import math
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import convolve, fftconvolve, oaconvolve, firwin, resample_poly
|
||||
|
||||
from experiments import lab046b_continuous_iq as iq
|
||||
from experiments import lab046c_offline_recovery as offline
|
||||
from experiments import lab050_streaming_receiver as previous
|
||||
|
||||
|
||||
phy = previous.phy
|
||||
lab045 = phy.lab045
|
||||
lab043 = lab045.lab044.lab043
|
||||
ORIGINAL_REFINE = lab043._refine_frame_start
|
||||
ORIGINAL_TIMING = lab043._preamble_timing_offset
|
||||
ORIGINAL_MATCHED = lab043._matched_waveform
|
||||
ORIGINAL_VITERBI = lab045.conv.viterbi_decode_soft
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def timing_kernel(samples_per_symbol: int) -> np.ndarray:
|
||||
"""Те же символы преамбулы на исходной сетке отсчётов."""
|
||||
expected = iq.radio.bpsk_modulate(lab045.lab044.preamble_bits())
|
||||
kernel = np.zeros((expected.size - 1) * samples_per_symbol + 1, np.complex64)
|
||||
kernel[::samples_per_symbol] = np.conj(expected)
|
||||
return kernel[::-1].copy()
|
||||
|
||||
|
||||
def fast_preamble_timing(matched: np.ndarray, nominal: int, expected_symbols: np.ndarray) -> int:
|
||||
"""Все прежние 1601 смещения; отрицательные/усечённые окна имеют score=0.
|
||||
|
||||
Корреляция с разреженной преамбулой эквивалентна vdot каждой выборки.
|
||||
Энергии получаются разностями сумм на каждой из 16 фаз. Диапазон не сужен.
|
||||
"""
|
||||
samples_per_symbol = lab045.SAMPLES_PER_SYMBOL
|
||||
offsets = np.arange(-800, 801)
|
||||
starts = nominal + offsets
|
||||
count = len(expected_symbols)
|
||||
# Тело не участвует ни в одном из прежних 1601 timing-окон.
|
||||
matched = matched[:nominal + 800 + (count - 1) * samples_per_symbol + 1]
|
||||
valid = (starts >= 0) & (starts + (count - 1) * samples_per_symbol < matched.size)
|
||||
scores = np.zeros(offsets.size, np.float64)
|
||||
if np.any(valid):
|
||||
kernel = timing_kernel(samples_per_symbol)
|
||||
correlations = fftconvolve(matched, kernel, mode="valid")
|
||||
padding = (-matched.size) % samples_per_symbol
|
||||
power = np.pad(np.abs(matched) ** 2, (0, padding)).reshape(-1, samples_per_symbol)
|
||||
cumulative = np.concatenate((np.zeros((1, samples_per_symbol)),
|
||||
np.cumsum(power, axis=0, dtype=np.float64))).ravel()
|
||||
selected = starts[valid]
|
||||
energy = cumulative[selected + count * samples_per_symbol] - cumulative[selected]
|
||||
scores[valid] = np.abs(correlations[selected]) / np.sqrt(count * np.maximum(energy, 0) + 1e-30)
|
||||
largest = np.partition(scores, -2)[-2:]
|
||||
if largest[-1] > 0 and largest[-1] - largest[-2] <= 32 * np.finfo(np.float32).eps * largest[-1]:
|
||||
# FFT меняет последние разряды. На неразделимых пиках сохраняем
|
||||
# исходное суммирование и его tie→первый offset, включая чистый тон.
|
||||
return ORIGINAL_TIMING(matched, nominal, expected_symbols)
|
||||
return int(offsets[int(np.argmax(scores))])
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def carrier_rotation(length: int) -> np.ndarray:
|
||||
"""Кэш переносов не очищается при каждом входном блоке старым адаптером."""
|
||||
indexes = np.arange(length)
|
||||
return np.exp(-2j * np.pi * lab043.CARRIER_OFFSET_HZ * indexes / lab045.SAMPLE_RATE_HZ).astype(np.complex64)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def dc_response(length: int, samples_per_symbol: int) -> np.ndarray:
|
||||
"""Отклик локального окна постоянной составляющей после переноса/RRC."""
|
||||
rotation = carrier_rotation(length)
|
||||
return oaconvolve(rotation, lab043._matched_filter_taps(), mode="full")
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def decimation_taps(decimation: int) -> np.ndarray:
|
||||
"""Штатные коэффициенты resample_poly(up=1, window=('kaiser', 8))."""
|
||||
return firwin(20 * decimation + 1, 1 / decimation, window=("kaiser", 8)).astype(np.float32)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def detection_rotation(length: int, decimation: int) -> np.ndarray:
|
||||
return np.exp(-2j * np.pi * lab045.CARRIER_OFFSET_HZ * np.arange(length)
|
||||
/ (lab045.SAMPLE_RATE_HZ / decimation)).astype(np.complex64)
|
||||
|
||||
|
||||
class DecimationCache:
|
||||
"""Повторно использовать внутреннюю часть FIR, пересчитать только края.
|
||||
|
||||
resample_poly имеет поддержку ±10 выходных отсчётов. После сдвига окна
|
||||
неизменная внутренняя область даёт ровно те же значения; левый нулевой
|
||||
край и новое продолжение считаются с прежними коэффициентами заново.
|
||||
"""
|
||||
|
||||
def __init__(self, decimation: int):
|
||||
self.decimation = decimation
|
||||
self.base = self.input_count = 0
|
||||
self.values = np.empty(0, np.complex64)
|
||||
self.reused_samples = self.filtered_input_samples = 0
|
||||
|
||||
def get(self, samples: np.ndarray, base: int) -> np.ndarray:
|
||||
stride = self.decimation
|
||||
count = math.ceil(samples.size / stride)
|
||||
shift = (base - self.base) // stride
|
||||
low = max(10, -shift + 10)
|
||||
high = min(count - 10, self.values.size - 11 - shift)
|
||||
|
||||
def filtered(array):
|
||||
self.filtered_input_samples += array.size
|
||||
return resample_poly(array, 1, stride, window=decimation_taps(stride))
|
||||
|
||||
if not self.values.size or (base - self.base) % stride or high <= low:
|
||||
output = filtered(samples)
|
||||
else:
|
||||
output = np.empty(count, np.complex64)
|
||||
output[low:high] = self.values[low + shift:high + shift]
|
||||
self.reused_samples += high - low
|
||||
prefix_end = min(samples.size, (low + 11) * stride)
|
||||
output[:low] = filtered(samples[:prefix_end])[:low]
|
||||
suffix_start = (high - 11) * stride
|
||||
output[high:] = filtered(samples[suffix_start:])[11:11 + count - high]
|
||||
self.values = output
|
||||
self.base, self.input_count = base, samples.size
|
||||
return output
|
||||
|
||||
|
||||
def fast_viterbi(soft_values: np.ndarray, information_bit_count: int) -> np.ndarray:
|
||||
"""Та же евклидова метрика и tie→branch 0, с готовыми метриками ветвей."""
|
||||
conv = lab045.conv
|
||||
if not isinstance(information_bit_count, (int, np.integer)) or information_bit_count <= 0:
|
||||
return ORIGINAL_VITERBI(soft_values, information_bit_count)
|
||||
values = np.asarray(soft_values, dtype=np.float64)
|
||||
if (values.ndim != 1 or values.size != conv.encoded_bit_count(information_bit_count)
|
||||
or not np.all(np.isfinite(values))):
|
||||
return ORIGINAL_VITERBI(soft_values, information_bit_count)
|
||||
step_count = int(information_bit_count) + conv.TAIL_BIT_COUNT
|
||||
received = values.reshape(step_count, 2)
|
||||
# Всего четыре различных пары BPSK-символов. Порядок арифметики исходный.
|
||||
negative = (received + 1.0) ** 2
|
||||
positive = (received - 1.0) ** 2
|
||||
metrics = np.column_stack((negative[:, 0] + negative[:, 1], negative[:, 0] + positive[:, 1],
|
||||
positive[:, 0] + negative[:, 1], positive[:, 0] + positive[:, 1]))
|
||||
symbols = conv._BRANCH_SYMBOLS
|
||||
codes = (2 * (symbols[:, :, 0] > 0) + (symbols[:, :, 1] > 0)).astype(np.intp)
|
||||
metric_zero = metrics[:, codes[:, 0]]
|
||||
metric_one = metrics[:, codes[:, 1]]
|
||||
predecessor_zero = conv._PREDECESSORS[:, 0]
|
||||
predecessor_one = conv._PREDECESSORS[:, 1]
|
||||
path = np.full(conv.STATE_COUNT, 1e18)
|
||||
path[0] = 0.0
|
||||
choices = np.empty((step_count, conv.STATE_COUNT), np.uint8)
|
||||
for step in range(step_count):
|
||||
zero = path[predecessor_zero] + metric_zero[step]
|
||||
one = path[predecessor_one] + metric_one[step]
|
||||
choices[step] = one < zero
|
||||
path = np.minimum(zero, one)
|
||||
decoded = np.empty(step_count, np.uint8)
|
||||
state = 0
|
||||
for step in range(step_count - 1, -1, -1):
|
||||
decoded[step] = state >> (conv.MEMORY - 1)
|
||||
state = int(conv._PREDECESSORS[state, choices[step, state]])
|
||||
assert state == 0
|
||||
return decoded[:information_bit_count]
|
||||
|
||||
|
||||
class BlockFrontEnd:
|
||||
"""Один ограниченный общий участок; кандидаты читают его представления.
|
||||
|
||||
Конечные края и локальное вычитание среднего восстановлены отдельно:
|
||||
нельзя без этого подменять фильтрацию изолированного окна непрерывной.
|
||||
"""
|
||||
|
||||
def __init__(self, samples: np.ndarray, owned_end: int, statistics: Counter,
|
||||
*, decimator: DecimationCache | None = None, base: int = 0):
|
||||
self.samples = samples
|
||||
self.statistics = statistics
|
||||
self.decimator, self.base = decimator, base
|
||||
self.reference = lab045.lab044._preamble_reference_samples()
|
||||
self.reference_length = (16 + lab043.PILOT_SYMBOL_COUNT + 10) * lab045.SAMPLES_PER_SYMBOL
|
||||
self.probe_symbols = lab045.PREAMBLE_BIT_COUNT + lab045.PHY_HEADER_BIT_COUNT
|
||||
self.probe_waveform = (self.probe_symbols + 42) * lab045.SAMPLES_PER_SYMBOL
|
||||
self.front_length = min(samples.size, owned_end + self.probe_waveform + offline.REFINEMENT_MARGIN_SAMPLES)
|
||||
self.owned_end = owned_end
|
||||
self.prefix_correlation = None
|
||||
self.changes = None
|
||||
self.matched = None
|
||||
self.baseband = None
|
||||
self.soft_cache = {}
|
||||
|
||||
def coarse_scores(self, samples: np.ndarray, reference: np.ndarray) -> np.ndarray:
|
||||
"""Сохранить DC всего старого окна, считать корреляцию только owned+1."""
|
||||
decimation = lab043.DETECTION_DECIMATION
|
||||
needed = math.ceil(self.owned_end / decimation) + reference.size + 1
|
||||
raw = (resample_poly(samples, 1, decimation, window=decimation_taps(decimation)) if self.decimator is None
|
||||
else self.decimator.get(samples, self.base))
|
||||
mean = np.mean(raw, dtype=np.complex128).astype(np.complex64)
|
||||
values = raw[:needed].copy()
|
||||
values -= mean
|
||||
values *= detection_rotation(values.size, decimation)
|
||||
if values.size < reference.size:
|
||||
return np.empty(0, np.float32)
|
||||
scores = lab043._normalized_sliding_correlation(values, reference).astype(np.float32)
|
||||
scores[~np.isfinite(scores)] = 0
|
||||
np.clip(scores, 0, 1, out=scores)
|
||||
return scores
|
||||
|
||||
def local_start(self, array: np.ndarray) -> int:
|
||||
return (array.ctypes.data - self.samples.ctypes.data) // self.samples.itemsize
|
||||
|
||||
def refine(self, samples: np.ndarray, reference: np.ndarray, coarse_start: int) -> int:
|
||||
start = self.local_start(samples)
|
||||
low = max(0, coarse_start - 2000)
|
||||
high = min(samples.size, coarse_start + self.reference_length + 2000)
|
||||
if high - low < self.reference_length or start < 0:
|
||||
return ORIGINAL_REFINE(samples, reference, coarse_start)
|
||||
if self.changes is None:
|
||||
self.changes = np.empty(self.front_length, np.int64)
|
||||
self.changes[0] = 0
|
||||
np.cumsum(self.samples[1:self.front_length] != self.samples[:self.front_length-1],
|
||||
dtype=np.int64, out=self.changes[1:])
|
||||
if self.changes[start + samples.size - 1] == self.changes[start]:
|
||||
# После вычитания локального среднего точно нулевой сигнал.
|
||||
# Общая FFT может оставить шум округления вместо нулевого tie.
|
||||
return low
|
||||
if self.prefix_correlation is None:
|
||||
centered = self.reference[:self.reference_length] - np.mean(self.reference[:self.reference_length])
|
||||
self.centered_reference_sum = np.sum(np.conj(centered), dtype=np.complex128)
|
||||
self.prefix_correlation = fftconvolve(
|
||||
self.samples[:self.front_length], np.conj(centered[::-1]), mode="valid")
|
||||
self.statistics["shared_refinement_correlations"] += 1
|
||||
first = start + low
|
||||
last = start + high - self.reference_length + 1
|
||||
correlation = self.prefix_correlation[first:last]
|
||||
if not correlation.size:
|
||||
return ORIGINAL_REFINE(samples, reference, coarse_start)
|
||||
# Та же DC-поправка, что у корреляции (samples - mean(probe)).
|
||||
correlation = correlation - np.mean(samples) * self.centered_reference_sum
|
||||
magnitudes = np.abs(correlation)
|
||||
if magnitudes.size > 1:
|
||||
largest = np.partition(magnitudes, -2)[-2:]
|
||||
if largest[-1] - largest[-2] <= 32*np.finfo(np.float32).eps*largest[-1]:
|
||||
self.statistics["refinement_tie_fallbacks"] += 1
|
||||
return ORIGINAL_REFINE(samples, reference, coarse_start)
|
||||
return low + int(np.argmax(magnitudes))
|
||||
|
||||
def matched_window(self, samples: np.ndarray, start: int, length: int):
|
||||
absolute = self.local_start(samples) + start
|
||||
taps = lab043._matched_filter_taps()
|
||||
edge = taps.size - 1
|
||||
if absolute < 0 or absolute + length + edge > self.front_length:
|
||||
self.statistics["local_body_matched_calls"] += 1
|
||||
segment = samples[start:start + length] - np.mean(samples)
|
||||
baseband = segment * carrier_rotation(length)
|
||||
return oaconvolve(baseband, taps, mode="full"), 16 * lab045.SAMPLES_PER_SYMBOL + edge
|
||||
if self.matched is None:
|
||||
self.baseband = self.samples[:self.front_length] * carrier_rotation(self.front_length)
|
||||
self.matched = oaconvolve(self.baseband, taps, mode="full")
|
||||
self.statistics["shared_matched_calls"] += 1
|
||||
matched = self.matched[absolute:absolute + length + edge].copy()
|
||||
# Восстановить нулевые края исходного локального окна, без соседнего IQ.
|
||||
matched[:edge] = convolve(self.baseband[absolute:absolute + edge], taps, method="direct")[:edge]
|
||||
matched[-edge:] = convolve(self.baseband[absolute + length - edge:absolute + length], taps,
|
||||
method="direct")[edge:]
|
||||
phase = np.complex64(np.exp(2j * np.pi * lab043.CARRIER_OFFSET_HZ * absolute / lab045.SAMPLE_RATE_HZ))
|
||||
matched *= phase
|
||||
mean = np.mean(samples)
|
||||
matched -= mean * dc_response(length, lab045.SAMPLES_PER_SYMBOL)
|
||||
nominal = 16 * lab045.SAMPLES_PER_SYMBOL + edge
|
||||
return matched, nominal
|
||||
|
||||
def timing(self, matched, nominal, expected):
|
||||
"""Локальный FFT оказался дешевле дополнительных общих timing-массивов."""
|
||||
return fast_preamble_timing(matched, nominal, expected)
|
||||
|
||||
def extract(self, samples: np.ndarray, frame_start: int, symbol_count: int) -> iq.SoftWindow:
|
||||
key = (self.local_start(samples) + frame_start, symbol_count)
|
||||
# Усечение зависит и от доступности текущего probe, а не только от start.
|
||||
length = (symbol_count + 42) * lab045.SAMPLES_PER_SYMBOL
|
||||
if frame_start < 0 or frame_start + length > samples.size:
|
||||
return iq.SoftWindow(False, np.empty(0), 0, 0, 0, {}, "TRUNCATED_IQ_WINDOW")
|
||||
if key in self.soft_cache:
|
||||
self.statistics["soft_window_cache_hits"] += 1
|
||||
return self.soft_cache[key]
|
||||
segment = samples[frame_start:frame_start + length]
|
||||
matched, nominal = lab043._matched_waveform(segment, 0, length)
|
||||
bits = lab045.lab044.preamble_bits()
|
||||
expected = iq.radio.bpsk_modulate(bits)
|
||||
timing = lab043._preamble_timing_offset(matched, nominal, expected)
|
||||
carrier = lab043._evaluate_symbols(matched, nominal, timing, bits, lab045.PREAMBLE_BIT_COUNT)
|
||||
if carrier["abs_h"] == 0:
|
||||
result = iq.SoftWindow(False, np.empty(0), timing, 0, nominal, carrier, "ZERO_GAIN")
|
||||
else:
|
||||
symbols = matched[nominal + timing:nominal + timing + symbol_count * lab045.SAMPLES_PER_SYMBOL:
|
||||
lab045.SAMPLES_PER_SYMBOL].astype(np.complex128)
|
||||
if symbols.size != symbol_count:
|
||||
result = iq.SoftWindow(False, np.empty(0), timing, 0, nominal, carrier, "TRUNCATED_SYMBOLS")
|
||||
else:
|
||||
symbol_time = (np.arange(symbol_count, dtype=np.float64) + 0.5) / lab045.SYMBOL_RATE
|
||||
normalized = symbols * np.exp(-2j * math.pi * carrier["cfo_hz"] * symbol_time)
|
||||
normalized /= carrier["abs_h"] * np.exp(1j * carrier["phase_rad"])
|
||||
result = iq.SoftWindow(True, normalized.real, int(timing),
|
||||
int((frame_start + nominal + timing) % lab045.SAMPLES_PER_SYMBOL),
|
||||
nominal, carrier, None)
|
||||
self.soft_cache[key] = result
|
||||
self.statistics["unique_soft_windows"] += 1
|
||||
return result
|
||||
|
||||
|
||||
@contextmanager
|
||||
def optimized_context(front: BlockFrontEnd):
|
||||
targets = ((lab043, "_refine_frame_start", front.refine),
|
||||
(lab043, "_matched_waveform", front.matched_window),
|
||||
(lab043, "_preamble_timing_offset", front.timing),
|
||||
(iq, "_extract_soft_window", front.extract),
|
||||
(lab045.conv, "viterbi_decode_soft", fast_viterbi),
|
||||
(offline, "_correlation_scores", front.coarse_scores))
|
||||
originals = [(owner, name, getattr(owner, name)) for owner, name, unused 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)
|
||||
|
||||
|
||||
class StreamingReceiver(previous.StreamingReceiver):
|
||||
"""Совместимый API Lab050, ускорение только для рабочей точки 500 ксимв/с."""
|
||||
|
||||
def __init__(self, symbol_rate: int = 500_000, *, block_samples: int = 65_536):
|
||||
if symbol_rate != 500_000:
|
||||
raise ValueError("Lab051 исследует только 500 ксимв/с")
|
||||
super().__init__(symbol_rate, block_samples=block_samples)
|
||||
self.front_statistics = Counter()
|
||||
self.decimator = DecimationCache(self.decimation)
|
||||
|
||||
def _scan(self, owned_end: int) -> list[previous.ReceivedPacket]:
|
||||
front = BlockFrontEnd(self.buffer, owned_end - self.base_sample, self.front_statistics,
|
||||
decimator=self.decimator, base=self.base_sample)
|
||||
with optimized_context(front):
|
||||
return super()._scan(owned_end)
|
||||
|
||||
def finish(self):
|
||||
result = super().finish()
|
||||
self.decimator.values = np.empty(0, np.complex64)
|
||||
return result
|
||||
|
||||
|
||||
def run_functional_tests() -> None:
|
||||
receiver = StreamingReceiver()
|
||||
assert receiver.mode.samples_per_symbol == 16
|
||||
assert receiver.finish() == []
|
||||
287
experiments/lab052_candidate_detection.py
Normal file
287
experiments/lab052_candidate_detection.py
Normal 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()
|
||||
157
experiments/lab052_detection_statistics.py
Normal file
157
experiments/lab052_detection_statistics.py
Normal 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()
|
||||
374
experiments/lab052_detector_study.py
Normal file
374
experiments/lab052_detector_study.py
Normal 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()
|
||||
@@ -4,7 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Iterable
|
||||
import math
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from protocol.link_packet import TrafficClass, encode_link_packet
|
||||
from protocol.priority_scheduler import transmission_duration_seconds
|
||||
@@ -35,6 +36,12 @@ class VideoFrameGroup:
|
||||
composite_frame_id: int
|
||||
generation_time_us: int
|
||||
packets: tuple[AgePolicyPacket, ...]
|
||||
available_time_us: int | None = None
|
||||
|
||||
@property
|
||||
def available_time_seconds(self) -> float:
|
||||
"""Готовность закодированного изображения отдельно от момента съёмки."""
|
||||
return (self.generation_time_us if self.available_time_us is None else self.available_time_us) / 1_000_000.0
|
||||
|
||||
@property
|
||||
def generation_time_seconds(self) -> float:
|
||||
@@ -90,6 +97,7 @@ class FrameScheduleResult:
|
||||
started_frame_ids: tuple[int, ...]
|
||||
completed_frame_ids: tuple[int, ...]
|
||||
admissions: tuple[FrameAdmission, ...]
|
||||
maximum_waiting_frames: int = 0
|
||||
|
||||
|
||||
def _replace_state(
|
||||
@@ -195,13 +203,23 @@ def schedule_video_frames(
|
||||
high_priority_packets: Iterable[AgePolicyPacket],
|
||||
policy: FramePolicy,
|
||||
channel_bitrate_bps: float,
|
||||
*,
|
||||
max_video_age_seconds: float | None = None,
|
||||
completion_latency_seconds: float | Callable[[VideoFrameGroup], float] = 0.0,
|
||||
) -> FrameScheduleResult:
|
||||
"""Run strict-priority service while keeping video frames contiguous."""
|
||||
|
||||
policy = FramePolicy(policy)
|
||||
if channel_bitrate_bps <= 0.0:
|
||||
raise ValueError("channel bitrate must be positive")
|
||||
frame_arrivals = tuple(sorted(frames, key=lambda frame: (frame.generation_time_seconds, frame.composite_frame_id)))
|
||||
if max_video_age_seconds is not None and (not math.isfinite(max_video_age_seconds) or max_video_age_seconds <= 0):
|
||||
raise ValueError("Возраст видео должен быть конечным положительным числом")
|
||||
if not callable(completion_latency_seconds) and (not math.isfinite(completion_latency_seconds) or completion_latency_seconds < 0):
|
||||
raise ValueError("Задержка после RF должна быть конечной и неотрицательной")
|
||||
frame_arrivals = tuple(sorted(frames, key=lambda frame: (frame.available_time_seconds, frame.composite_frame_id)))
|
||||
for frame in frame_arrivals:
|
||||
if not frame.packets or frame.available_time_seconds < frame.generation_time_seconds:
|
||||
raise ValueError("Нужны непустые изображения, готовые не раньше съёмки")
|
||||
high_arrivals = tuple(sorted(high_priority_packets, key=lambda item: (item.available_time_seconds, item.arrival_order)))
|
||||
ready_high: list[AgePolicyPacket] = []
|
||||
pending_frames: list[VideoFrameGroup] = []
|
||||
@@ -214,15 +232,17 @@ def schedule_video_frames(
|
||||
actual_by_frame: dict[int, float] = {}
|
||||
active: VideoFrameGroup | None = None
|
||||
active_index = 0
|
||||
maximum_waiting = 0
|
||||
newest_capture_key: tuple[int, int] | None = None
|
||||
cursor = 0.0
|
||||
frame_index = high_index = 0
|
||||
blocker_by_order: dict[int, tuple[AgePolicyPacket, float]] = {}
|
||||
|
||||
def drop_frame(frame: VideoFrameGroup, reason: str) -> None:
|
||||
dropped.append(FrameDrop(frame, cursor, reason))
|
||||
def drop_frame(frame: VideoFrameGroup, reason: str, when: float | None = None) -> None:
|
||||
dropped.append(FrameDrop(frame, cursor if when is None else when, reason))
|
||||
|
||||
def admit(now: float, active_packet: AgePolicyPacket | None = None, active_end: float = 0.0) -> None:
|
||||
nonlocal frame_index, high_index
|
||||
nonlocal frame_index, high_index, maximum_waiting, newest_capture_key
|
||||
while high_index < len(high_arrivals) and high_arrivals[high_index].available_time_seconds <= now + TIME_EPSILON_SECONDS:
|
||||
item = high_arrivals[high_index]
|
||||
high_index += 1
|
||||
@@ -232,12 +252,19 @@ def schedule_video_frames(
|
||||
max(0.0, active_end - item.available_time_seconds),
|
||||
)
|
||||
_replace_state(ready_high, item, replacements)
|
||||
while frame_index < len(frame_arrivals) and frame_arrivals[frame_index].generation_time_seconds <= now + TIME_EPSILON_SECONDS:
|
||||
while frame_index < len(frame_arrivals) and frame_arrivals[frame_index].available_time_seconds <= now + TIME_EPSILON_SECONDS:
|
||||
frame = frame_arrivals[frame_index]
|
||||
frame_index += 1
|
||||
if policy is FramePolicy.LATEST_ONLY:
|
||||
capture_key = (frame.generation_time_us, frame.composite_frame_id)
|
||||
# Позднее завершение кодирования не делает старый снимок новым.
|
||||
# Помним свежесть и когда очередь уже опустела/новый кадр активен.
|
||||
if newest_capture_key is not None and capture_key < newest_capture_key:
|
||||
drop_frame(frame, "older_capture_arrived_late", frame.available_time_seconds)
|
||||
continue
|
||||
newest_capture_key = capture_key
|
||||
for old in pending_frames:
|
||||
drop_frame(old, "replaced_by_newest")
|
||||
drop_frame(old, "replaced_by_newest", frame.available_time_seconds)
|
||||
pending_frames[:] = [frame]
|
||||
elif policy is FramePolicy.TWO_WAITING:
|
||||
pending_frames.append(frame)
|
||||
@@ -245,6 +272,7 @@ def schedule_video_frames(
|
||||
drop_frame(pending_frames.pop(0), "waiting_limit")
|
||||
else:
|
||||
pending_frames.append(frame)
|
||||
maximum_waiting = max(maximum_waiting, len(pending_frames))
|
||||
|
||||
while (
|
||||
frame_index < len(frame_arrivals)
|
||||
@@ -256,7 +284,7 @@ def schedule_video_frames(
|
||||
if not ready_high and not pending_frames and active is None:
|
||||
next_times = []
|
||||
if frame_index < len(frame_arrivals):
|
||||
next_times.append(frame_arrivals[frame_index].generation_time_seconds)
|
||||
next_times.append(frame_arrivals[frame_index].available_time_seconds)
|
||||
if high_index < len(high_arrivals):
|
||||
next_times.append(high_arrivals[high_index].available_time_seconds)
|
||||
cursor = max(cursor, min(next_times))
|
||||
@@ -273,7 +301,7 @@ def schedule_video_frames(
|
||||
if active is None:
|
||||
while pending_frames and active is None:
|
||||
candidate = pending_frames.pop(0)
|
||||
deadline = policy.deadline_seconds
|
||||
deadline = max_video_age_seconds if max_video_age_seconds is not None else policy.deadline_seconds
|
||||
if deadline is not None:
|
||||
future_high = high_arrivals[high_index:]
|
||||
predicted = predict_frame_completion(
|
||||
@@ -283,7 +311,11 @@ def schedule_video_frames(
|
||||
tuple(ready_high),
|
||||
future_high,
|
||||
)
|
||||
if predicted - candidate.generation_time_seconds > deadline + TIME_EPSILON_SECONDS:
|
||||
post_latency = (completion_latency_seconds(candidate) if callable(completion_latency_seconds)
|
||||
else completion_latency_seconds)
|
||||
if not math.isfinite(post_latency) or post_latency < 0:
|
||||
raise ValueError("Прогноз задержки должен быть конечным и неотрицательным")
|
||||
if predicted + post_latency - candidate.generation_time_seconds > deadline + TIME_EPSILON_SECONDS:
|
||||
drop_frame(candidate, "prediction_reject")
|
||||
continue
|
||||
predicted_by_frame[candidate.composite_frame_id] = predicted
|
||||
@@ -327,4 +359,5 @@ def schedule_video_frames(
|
||||
started_frame_ids=tuple(started),
|
||||
completed_frame_ids=tuple(completed),
|
||||
admissions=admissions,
|
||||
maximum_waiting_frames=maximum_waiting,
|
||||
)
|
||||
|
||||
176
tests/test_lab050_video_stream.py
Normal file
176
tests/test_lab050_video_stream.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Свежесть изображений и непрерывный IQ, без файлов эксперимента и RF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from experiments import lab049_bpsk_symbol_rate as phy
|
||||
from experiments import lab050_video_stream as lab
|
||||
from experiments.lab050_streaming_receiver import StreamingReceiver
|
||||
from protocol.two_stage_failsafe import TwoStageFailsafe, ThresholdPolicy, DecelerationProfile, SafetyState
|
||||
|
||||
|
||||
def sequence(size: int = 1740, count: int = 30) -> list[lab.EncodedImage]:
|
||||
return [lab.EncodedImage(index+1, bytes([index % 256])*size, .001, .001, 0.0) for index in range(count)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rate,fps,maximum", [(500000,3,6832),(500000,5,4148),(500000,10,1952),
|
||||
(800000,3,11224),(800000,5,6588),(800000,10,3172),
|
||||
(1000000,3,13908),(1000000,5,8296),(1000000,10,4148)])
|
||||
def test_maximum_jpeg_is_exact_at_fragment_boundary(rate, fps, maximum):
|
||||
assert lab.maximum_jpeg_bytes(rate, fps) == maximum
|
||||
assert lab.fragment_count(maximum) * 5766 * fps <= rate
|
||||
assert lab.fragment_count(maximum+1) * 5766 * fps > rate
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fps", [3, 5, 10])
|
||||
@pytest.mark.parametrize("size", [244, 245, 1740, 8724])
|
||||
def test_newest_queue_is_bounded_and_accounting_conserves_inputs(fps, size):
|
||||
result = lab.simulate_sequence(sequence(size), fps, 200000, 1.0)
|
||||
assert result["maximum_waiting_frames"] <= 1
|
||||
assert result["input_frames"] == result["transmitted_frames"] + result["dropped_before_tx"]
|
||||
assert result["received_frames"] == result["transmitted_frames"]
|
||||
assert result["displayed_frames"] == result["received_frames"]
|
||||
assert result["image_age_max_seconds"] is None or result["image_age_max_seconds"] <= 1.0
|
||||
|
||||
|
||||
def test_newer_unstarted_frame_replaces_old_without_damaging_active_image():
|
||||
result = lab.simulate_sequence(sequence(), 10, 200000, 1.0)
|
||||
assert result["drop_reasons"]["replaced_by_newest"] > 0
|
||||
sent = [frame for frame in result["frames"] if frame["first_tx_timestamp"] is not None]
|
||||
assert all(frame["rf_age_seconds"] == pytest.approx(8*5766/200000) for frame in sent)
|
||||
assert all(frame["last_tx_timestamp"] <= next_frame["first_tx_timestamp"] for frame,next_frame in zip(sent,sent[1:]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("limit", [.2,.333,.5,1.0])
|
||||
def test_age_limit_predictively_drops_impossible_jpeg(limit):
|
||||
result = lab.simulate_sequence(sequence(8724), 5, 200000, limit)
|
||||
assert result["transmitted_frames"] == 0
|
||||
assert result["dropped_before_tx"] == 30
|
||||
assert result["fresh_display_fps"] == 0
|
||||
|
||||
|
||||
def test_source_time_not_encoding_completion_defines_age():
|
||||
images=[lab.EncodedImage(1,bytes(244),.3,.001,0)]
|
||||
result = lab.simulate_sequence(images,5,1000000,.2)
|
||||
assert result["transmitted_frames"] == 0
|
||||
|
||||
|
||||
def test_late_old_encoding_does_not_replace_fresher_waiting_image():
|
||||
images = [lab.EncodedImage(index+1,bytes(1740),delay,.001,0)
|
||||
for index,delay in enumerate((.001,.12,.001))]
|
||||
result=lab.simulate_sequence(images,10,200000,1.0)
|
||||
shown=[row["image_id"] for row in result["frames"] if row["display_timestamp"] is not None]
|
||||
assert shown==[1,3]
|
||||
assert result["drop_reasons"]["older_capture_arrived_late"]==1
|
||||
|
||||
|
||||
def test_late_old_encoding_does_not_regress_display_after_queue_drains():
|
||||
images = [lab.EncodedImage(index+1,bytes(244),delay,.001,0)
|
||||
for index,delay in enumerate((.5,.001,.001))]
|
||||
result=lab.simulate_sequence(images,5,1000000,1.0)
|
||||
shown=[row["image_id"] for row in result["frames"] if row["display_timestamp"] is not None]
|
||||
assert shown==[2,3]
|
||||
assert result["drop_reasons"]["older_capture_arrived_late"]==1
|
||||
|
||||
|
||||
def test_five_fresh_images_per_second_and_all_timestamps():
|
||||
result = lab.simulate_sequence(sequence(),5,500000,.2)
|
||||
assert result["fresh_display_fps"] == 5
|
||||
assert result["raw_display_fps"] == 5
|
||||
assert result["image_age_max_seconds"] == pytest.approx(.001+8*5766/500000+.001)
|
||||
for row in result["frames"]:
|
||||
names = ["capture_timestamp","encode_complete_timestamp","queue_enter_timestamp","first_tx_timestamp",
|
||||
"first_rx_timestamp","last_tx_timestamp","object_complete_timestamp","decode_complete_timestamp","display_timestamp"]
|
||||
values = [row[name] for name in names]
|
||||
assert values == sorted(values)
|
||||
|
||||
|
||||
def test_slow_rx_drops_before_cpu_and_does_not_claim_reception():
|
||||
result = lab.simulate_sequence(sequence(),5,500000,.2,rx_seconds_per_packet=.1)
|
||||
assert result["rf_delivered_objects"] == 0
|
||||
assert result["received_frames"] == result["displayed_frames"] == 0
|
||||
assert result["dropped_before_tx"] == 30
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fps,cost", [(5,.016),(10,.012)])
|
||||
def test_conditional_receiver_speed_meets_freshness_and_rate(fps,cost):
|
||||
result=lab.simulate_sequence(sequence(),fps,500000,.2,rx_seconds_per_packet=cost,
|
||||
receiver_lookahead_seconds=.052608)
|
||||
assert result["fresh_display_fps"] == (5 if fps==5 else 29/3)
|
||||
assert result["steady_fresh_display_fps"] == pytest.approx(fps)
|
||||
assert result["displayed_frames"] == 30
|
||||
assert result["drain_displayed_frames"] == (0 if fps==5 else 1)
|
||||
assert result["image_age_max_seconds"] < .2
|
||||
|
||||
|
||||
def test_sufficient_cpu_throughput_alone_does_not_guarantee_age():
|
||||
# 8*.02=.16 с/изображение укладывается в период .2 с, но сквозной возраст
|
||||
# с накоплением нужного хвоста IQ уже превышает .2 с.
|
||||
result=lab.simulate_sequence(sequence(),5,500000,.2,rx_seconds_per_packet=.02,
|
||||
receiver_lookahead_seconds=.052608)
|
||||
assert result["transmitted_frames"] == 0
|
||||
|
||||
|
||||
def test_elrs_loss_is_external_to_video_queue():
|
||||
before=lab.simulate_sequence(sequence(count=3),5,500000,.2)
|
||||
safety=TwoStageFailsafe(ThresholdPolicy("test",.1,.2),DecelerationProfile("test",1,2))
|
||||
assert safety.apply_watchdog(.2) is SafetyState.STAGE2_BRAKING
|
||||
after=lab.simulate_sequence(sequence(count=3),5,500000,.2)
|
||||
assert before==after
|
||||
assert not after["elrs_control_in_video_queue"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rate", [200000,400000,500000,800000,1000000])
|
||||
def test_stream_crosses_blocks_and_emits_before_finish(rate):
|
||||
payload=bytes(range(256))
|
||||
plan=phy.lab045.build_transmission_plan(payload)
|
||||
receiver=StreamingReceiver(rate)
|
||||
waveform=phy.build_parameterized_tx_complex(plan,phy.PhyMode(rate)).astype(np.complex64)
|
||||
signal=np.concatenate((np.zeros(63217,np.complex64),waveform,
|
||||
np.zeros(receiver.lookahead+2*receiver.core_samples,np.complex64)))
|
||||
outputs=[]
|
||||
chunk=32749
|
||||
for start in range(0,len(signal),chunk):
|
||||
outputs.extend(receiver.push_iq_block(signal[start:start+chunk]))
|
||||
assert receiver.buffer.size <= receiver.buffer_limit_samples
|
||||
assert [item.payload for item in outputs] == [payload]
|
||||
assert outputs[0].start_sample == pytest.approx(63217,abs=100)
|
||||
assert not receiver.finish()
|
||||
assert receiver.buffer.size == 0
|
||||
|
||||
|
||||
def test_memory_bound_for_long_session_without_retained_history():
|
||||
receiver=StreamingReceiver(1000000)
|
||||
block=np.zeros(receiver.block_samples,np.complex64)
|
||||
limits=[]
|
||||
for _ in range(100):
|
||||
assert receiver.push_iq_block(block)==[]
|
||||
limits.append(receiver.buffer.size)
|
||||
assert max(limits) <= receiver.buffer_limit_samples
|
||||
assert receiver.total_samples > 20*receiver.buffer_limit_samples
|
||||
assert receiver.packet_count == 0
|
||||
assert not receiver.finish()
|
||||
|
||||
|
||||
def test_truncated_stream_does_not_accept_partial_frame():
|
||||
plan=phy.lab045.build_transmission_plan(bytes(256))
|
||||
receiver=StreamingReceiver(1000000)
|
||||
waveform=phy.build_parameterized_tx_complex(plan,phy.PhyMode(1000000)).astype(np.complex64)
|
||||
receiver.push_iq_block(waveform[:len(waveform)//2])
|
||||
assert receiver.finish()==[]
|
||||
with pytest.raises(RuntimeError): receiver.push_iq_block(np.zeros(1,np.complex64))
|
||||
|
||||
|
||||
def test_input_limit_and_nonfinite_rejected_without_advancing_stream():
|
||||
receiver=StreamingReceiver(1000000)
|
||||
for block in [np.zeros(receiver.block_samples+1,np.complex64),np.array([complex(float("nan"),0)])]:
|
||||
with pytest.raises(ValueError): receiver.push_iq_block(block)
|
||||
assert receiver.total_samples==0
|
||||
|
||||
|
||||
def test_fixed_formats_match_actual_plan():
|
||||
frame=lab.transport.build_object_frames(bytes(1740))[0]
|
||||
assert frame.plan.frame_symbol_count+42==lab.FRAME_INTERVALS
|
||||
assert lab.transport.FRAGMENT_DATA_SIZE==244
|
||||
160
tests/test_lab051_streaming_receiver.py
Normal file
160
tests/test_lab051_streaming_receiver.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Регрессии вычислительной архитектуры Lab051, без файлов/оборудования."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from dataclasses import replace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy.signal import resample_poly
|
||||
|
||||
from experiments import lab051_streaming_receiver as lab
|
||||
from experiments.lab050_streaming_receiver import StreamingReceiver as PreviousReceiver
|
||||
from experiments.lab050_streaming_receiver import streaming_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", range(8))
|
||||
def test_all_timing_offsets_preserved_including_negative_start(seed):
|
||||
random_generator = np.random.default_rng(seed)
|
||||
samples = (random_generator.normal(size=23616) + 1j * random_generator.normal(size=23616)).astype(np.complex64)
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
expected = lab.iq.radio.bpsk_modulate(lab.lab045.lab044.preamble_bits())
|
||||
if seed % 2:
|
||||
samples[416 + 47:416 + 47 + len(expected)*16:16] += expected * 3
|
||||
old = lab.ORIGINAL_TIMING(samples, 416, expected)
|
||||
assert lab.fast_preamble_timing(samples, 416, expected) == old
|
||||
|
||||
|
||||
def test_zero_timing_tie_is_same_first_offset():
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
expected = lab.iq.radio.bpsk_modulate(lab.lab045.lab044.preamble_bits())
|
||||
assert lab.fast_preamble_timing(np.zeros(23616, np.complex64), 416, expected) == -800
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [1, 1j, 1+1j, 44])
|
||||
def test_equal_nonzero_timing_scores_preserve_original_tie(value):
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
expected = lab.iq.radio.bpsk_modulate(lab.lab045.lab044.preamble_bits())
|
||||
samples = np.full(23616, value, np.complex64)
|
||||
assert lab.fast_preamble_timing(samples, 416, expected) == lab.ORIGINAL_TIMING(samples,416,expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [1, 1j, 44])
|
||||
def test_refinement_constant_iq_preserves_zero_after_local_dc(value):
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
samples=np.full(100000,value,np.complex64)
|
||||
front=lab.BlockFrontEnd(samples,65536,Counter())
|
||||
window=samples[1000:28456]
|
||||
assert front.refine(window,front.reference,2000)==lab.ORIGINAL_REFINE(window,front.reference,2000)==0
|
||||
|
||||
|
||||
def test_refinement_equal_nonzero_peaks_preserve_first_maximum():
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
samples=np.zeros(100000,np.complex64)
|
||||
samples[1200]=samples[2200]=1
|
||||
front=lab.BlockFrontEnd(samples,65536,Counter())
|
||||
reference=np.zeros(front.reference_length,np.complex64)
|
||||
reference[0],reference[1]=1,-1
|
||||
front.reference=reference
|
||||
window=samples[1000:28456]
|
||||
assert front.refine(window,reference,2000)==lab.ORIGINAL_REFINE(window,reference,2000)==199
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", range(5))
|
||||
def test_shared_matched_preserves_local_dc_and_both_zero_padded_edges(seed):
|
||||
random_generator = np.random.default_rng(seed)
|
||||
values = (random_generator.normal(size=110000) + 1j * random_generator.normal(size=110000) + 4 + 3j).astype(np.complex64)
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
front = lab.BlockFrontEnd(values, 70000, Counter())
|
||||
segment = values[5000 + seed:5000 + seed + 23456]
|
||||
expected, nominal = lab.ORIGINAL_MATCHED(segment, 0, len(segment))
|
||||
actual, actual_nominal = front.matched_window(segment, 0, len(segment))
|
||||
assert actual_nominal == nominal
|
||||
np.testing.assert_allclose(actual, expected, atol=2e-5, rtol=2e-5)
|
||||
|
||||
|
||||
def test_reused_decimation_is_bit_exact_for_moving_and_truncated_windows():
|
||||
random_generator = np.random.default_rng(51001)
|
||||
values = (random_generator.normal(size=1800000) + 1j * random_generator.normal(size=1800000)).astype(np.complex64)
|
||||
cache = lab.DecimationCache(8)
|
||||
for base in range(0, len(values) - 10000, 65536):
|
||||
window = values[base:base + 400123]
|
||||
expected = resample_poly(window, 1, 8, window=("kaiser", 8))
|
||||
np.testing.assert_array_equal(cache.get(window, base), expected)
|
||||
assert cache.values.size <= 50016
|
||||
assert cache.reused_samples > 1000000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed,count", [(0, 1), (1, 32), (2, 244*8), (3, 268*8), (4, 1024*8)])
|
||||
def test_viterbi_exact_bits_for_noisy_metrics_and_ties(seed, count):
|
||||
random_generator = np.random.default_rng(seed)
|
||||
for values in (np.zeros(lab.lab045.conv.encoded_bit_count(count)),
|
||||
random_generator.normal(size=lab.lab045.conv.encoded_bit_count(count)),
|
||||
random_generator.choice([-1., 1.], size=lab.lab045.conv.encoded_bit_count(count))):
|
||||
np.testing.assert_array_equal(lab.fast_viterbi(values, count), lab.ORIGINAL_VITERBI(values, count))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("values,count", [(np.array([np.nan, 0.]), 1), (np.zeros((2,2)), 1),
|
||||
(np.zeros(12), 1), (np.zeros(12), 0), (np.zeros(12), 1.5)])
|
||||
def test_viterbi_rejects_same_invalid_input(values, count):
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
lab.fast_viterbi(values, count)
|
||||
|
||||
|
||||
def signal_with_unknown_offsets(seed=51):
|
||||
random_generator = np.random.default_rng(seed)
|
||||
plan = lab.lab045.build_transmission_plan(bytes(range(256)))
|
||||
values = lab.phy.build_parameterized_tx_complex(plan, lab.phy.PhyMode(500000)).astype(np.complex64)
|
||||
values *= np.exp(1j * (0.73 + 2*np.pi*10*np.arange(len(values))/8000000)).astype(np.complex64)
|
||||
values += (random_generator.normal(size=len(values)) + 1j*random_generator.normal(size=len(values))).astype(np.complex64)
|
||||
return np.concatenate((np.zeros(63217,np.complex64), values, np.zeros(450000,np.complex64)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("block", [32749, 65536])
|
||||
def test_independent_stream_packets_equal_old_decoder_across_block_boundaries(block):
|
||||
samples = signal_with_unknown_offsets()
|
||||
results = []
|
||||
for receiver in (PreviousReceiver(500000), lab.StreamingReceiver()):
|
||||
output = []
|
||||
for start in range(0,len(samples),block):
|
||||
output.extend(receiver.push_iq_block(samples[start:start + block]))
|
||||
assert [item.payload for item in output] == [bytes(range(256))]
|
||||
output.extend(receiver.finish())
|
||||
results.append([(item.start_sample,item.end_sample,item.payload) for item in output])
|
||||
assert results[0] == results[1]
|
||||
|
||||
|
||||
def test_invalid_push_and_finish_preserve_bounded_state():
|
||||
receiver = lab.StreamingReceiver()
|
||||
for invalid in (np.array([complex(float("nan"),0)]), np.zeros((1,2)), np.zeros(65537)):
|
||||
with pytest.raises(ValueError): receiver.push_iq_block(invalid)
|
||||
assert receiver.total_samples == 0
|
||||
for unused in range(100):
|
||||
assert receiver.push_iq_block(np.zeros(65536, np.complex64)) == []
|
||||
assert receiver.buffer.size <= receiver.buffer_limit_samples
|
||||
assert receiver.decimator.values.size <= (receiver.buffer_limit_samples + 7)//8
|
||||
assert receiver.finish() == []
|
||||
assert receiver.buffer.size == receiver.decimator.values.size == 0
|
||||
with pytest.raises(RuntimeError): receiver.finish()
|
||||
with pytest.raises(RuntimeError): receiver.push_iq_block(np.zeros(1,np.complex64))
|
||||
|
||||
|
||||
def test_truncated_frame_cannot_reach_crc_pass():
|
||||
samples = signal_with_unknown_offsets()[:100000]
|
||||
receiver = lab.StreamingReceiver()
|
||||
output = []
|
||||
for start in range(0,len(samples),65536):
|
||||
output.extend(receiver.push_iq_block(samples[start:start+65536]))
|
||||
assert output + receiver.finish() == []
|
||||
|
||||
|
||||
def test_context_restores_functions_even_on_exception():
|
||||
originals = (lab.lab043._matched_waveform, lab.iq._extract_soft_window,
|
||||
lab.lab045.conv.viterbi_decode_soft, lab.offline._correlation_scores)
|
||||
with streaming_mode(lab.phy.PhyMode(500000)):
|
||||
front = lab.BlockFrontEnd(np.zeros(100000,np.complex64),65536,Counter())
|
||||
with pytest.raises(RuntimeError):
|
||||
with lab.optimized_context(front): raise RuntimeError("test")
|
||||
assert originals == (lab.lab043._matched_waveform, lab.iq._extract_soft_window,
|
||||
lab.lab045.conv.viterbi_decode_soft, lab.offline._correlation_scores)
|
||||
211
tests/test_lab052_candidate_detection.py
Normal file
211
tests/test_lab052_candidate_detection.py
Normal 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)
|
||||
Reference in New Issue
Block a user