Lab051: accelerate 500 ksymbol/s streaming receiver
This commit is contained in:
105
PROJECT_LOG.md
105
PROJECT_LOG.md
@@ -1754,3 +1754,108 @@ Lab050 git add/commit и push/pull/fetch не выполнялись.
|
|||||||
и ускорение строгой проверки ложных кандидатов на C/500k с сохранением
|
и ускорение строгой проверки ложных кандидатов на C/500k с сохранением
|
||||||
независимости RX и FEC/CRC. До доказанной скорости и свежести не выполнять
|
независимости RX и FEC/CRC. До доказанной скорости и свежести не выполнять
|
||||||
RF, QPSK, новый видеокодек или перенос SDR на 180 МГц.
|
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 ксимв/с.
|
||||||
|
|||||||
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
@@ -275,6 +275,39 @@
|
|||||||
- **Далее:** Lab051 — профиль и ускорение строгой обработки ложных кандидатов.
|
- **Далее:** Lab051 — профиль и ускорение строгой обработки ложных кандидатов.
|
||||||
- **RF:** RF TX = 0. Lab050 commit/push не выполнялись. STOP.
|
- **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.
|
||||||
|
|
||||||
## Правило хранения данных
|
## Правило хранения данных
|
||||||
|
|
||||||
Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные
|
Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные
|
||||||
|
|||||||
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.
|
||||||
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() == []
|
||||||
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)
|
||||||
Reference in New Issue
Block a user