Lab042: validate Pluto image hardware loopback
@@ -538,3 +538,56 @@ Lab042: первая передача изображения через реал
|
|||||||
## Следующий этап
|
## Следующий этап
|
||||||
|
|
||||||
Кабельная петля через аттенюатор 30 дБ после получения переходника папа-папа. Это и есть заявленный опыт Lab042 с контролируемыми уровнями, его результат станет основным для лабораторной. Затем Lab043 — разведение на два устройства.
|
Кабельная петля через аттенюатор 30 дБ после получения переходника папа-папа. Это и есть заявленный опыт Lab042 с контролируемыми уровнями, его результат станет основным для лабораторной. Затем Lab043 — разведение на два устройства.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Запись 020
|
||||||
|
|
||||||
|
## Дата
|
||||||
|
|
||||||
|
11 августа 2026 года
|
||||||
|
|
||||||
|
## Тема
|
||||||
|
|
||||||
|
Lab042 завершена: кабельная петля через аттенюатор, зависимость доставки от мощности, устранение трёх дефектов отчётности.
|
||||||
|
|
||||||
|
## Схема опыта
|
||||||
|
|
||||||
|
Выход передатчика PlutoSDR через кабель SMA, аттенюатор AT30S 30 дБ, переходник и второй кабель на вход приёмника того же устройства. Антенны сняты. Это заявленный основной опыт лабораторной с контролируемыми уровнями, в отличие от вспомогательной эфирной проверки Записи 019.
|
||||||
|
|
||||||
|
## Зависимость доставки от мощности передатчика
|
||||||
|
|
||||||
|
Кабельный тракт при неизменной схеме исследован по усилению передатчика. Получен отчётливый переход от отказа к устойчивой передаче:
|
||||||
|
|
||||||
|
- TX минус 30 дБ: 0 из 10 фрагментов, контрольная сумма не сходится ни разу, средняя корреляция 0,936. Сигнал ниже порога уверенного приёма.
|
||||||
|
- TX минус 20 дБ: 8 из 10 фрагментов, средняя корреляция 0,992. Пограничный режим: часть кадров проходит.
|
||||||
|
- TX минус 10 дБ: 10 из 10 фрагментов, изображение собрано побайтово. Устойчивая передача.
|
||||||
|
|
||||||
|
Значения TX минус 30 и минус 20 дБ — исследование порога, повторять их не требуется. Рабочая точка кабельной петли: TX минус 10 дБ, RX плюс 20 дБ, аттенюатор 30 дБ, несущая 435 МГц.
|
||||||
|
|
||||||
|
Начальные умолчания кабельного режима TX минус 30 и RX плюс 20 дБ на железе прежде не проверялись; отказ первого прогона объяснялся именно этим, а не дефектом обработки.
|
||||||
|
|
||||||
|
## Результат рабочей точки
|
||||||
|
|
||||||
|
- Найдено 10 кадров из 10, контрольная сумма сошлась у всех десяти, восстановлены все фрагменты.
|
||||||
|
- Изображение собрано и совпадает с переданным. Совпадение подтверждено независимо: SHA-256 принятого файла сверен с SHA-256 переданного JPEG, пересобранного из исходника отдельно от прогона. Оба равны 751d7ea0d8711e2ef54fc5cf209a809cfee5bbb14916dd20aa201d5285b40286.
|
||||||
|
- Размер JPEG 4756 байт, качество 30, десять фрагментов.
|
||||||
|
- Длительность сигнала около 2,07 секунды, канальная скорость 18,404 кбит/с.
|
||||||
|
- Кадр 8 прошёл контрольную сумму при пониженной корреляции поиска 0,866 против 0,998 у остальных: низкая корреляция маркера не означает испорченных данных, фаза дискретизации была подобрана и кадр демодулировался чисто.
|
||||||
|
|
||||||
|
## Устранённые дефекты отчётности
|
||||||
|
|
||||||
|
Разбор кабельного прогона вскрыл три дефекта, при которых провал мог быть принят за успех. Все три устранены, каждый закрыт быстрым тестом в tests/test_lab042_reporting.py.
|
||||||
|
|
||||||
|
- Величина вектора ошибки нормировалась на единичный эталон, из-за чего при слабом сигнале становилась искусственно малой: провалившийся прогон показывал EVM меньше, чем успешный. Нормировка исправлена на мощность подогнанного сигнала. При невозможности вычислить величину возвращается признак невычислимости, а не ноль.
|
||||||
|
- Отчёт перечислял lab042_received.jpg среди созданных файлов безусловно, даже когда изображение не собрано и файла нет. Теперь файл указывается только при фактической сборке.
|
||||||
|
- Программа завершалась кодом ноль при полном отказе передачи. Введён критерий приёмки: при несобранном изображении код завершения ненулевой, чтобы автоматика не приняла провал за успех.
|
||||||
|
|
||||||
|
## Проверки
|
||||||
|
|
||||||
|
- Быстрые тесты ядра расширены с 125 до 135. Добавленные проверяют неуспешный путь: провал даёт ненулевой код, отсутствующий файл не объявляется созданным, невычислимая метрика не превращается в ноль.
|
||||||
|
- Окончательный кабельный прогон рабочей точки выполнен уже исправленной версией программы, поэтому итоговый отчёт сформирован ею.
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
Lab043: передаёт PlutoSDR, принимает RTL-SDR. Общий опорный генератор исчезает, впервые появляется реальное рассогласование несущей и частоты дискретизации двух независимых устройств. Здесь понадобится защищённая частотная коррекция, перенесённая в Записи 019.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
## 4. Текущее состояние
|
## 4. Текущее состояние
|
||||||
|
|
||||||
**Выполнено:** лабораторные Lab001–Lab041. Пройден путь от структуры пакета до сеансов связи с защитой от пакетов предыдущего сеанса.
|
**Выполнено:** лабораторные Lab001–Lab042. Пройден путь от структуры пакета до первой передачи изображения через реальный радиотракт.
|
||||||
|
|
||||||
Что реализовано и проверено:
|
Что реализовано и проверено:
|
||||||
|
|
||||||
@@ -45,11 +45,12 @@
|
|||||||
- блочное исправление стираний над GF(256), перемежение, подбор параметров;
|
- блочное исправление стираний над GF(256), перемежение, подбор параметров;
|
||||||
- приоритетное обслуживание команд, телеметрии и видео;
|
- приоритетное обслуживание команд, телеметрии и видео;
|
||||||
- двухступенчатая безопасная остановка, сторожевой таймер, постоянное аварийное намерение, безопасный сброс;
|
- двухступенчатая безопасная остановка, сторожевой таймер, постоянное аварийное намерение, безопасный сброс;
|
||||||
- идентификаторы сеанса, безопасный запуск, отклонение пакетов предыдущего сеанса.
|
- идентификаторы сеанса, безопасный запуск, отклонение пакетов предыдущего сеанса;
|
||||||
|
- передача JPEG через настоящий радиотракт на одном PlutoSDR: сначала по эфиру на паре антенн, затем по кабелю через аттенюатор 30 дБ. Принятое изображение совпало с переданным побайтово (Записи 019 и 020).
|
||||||
|
|
||||||
**Что ещё не сделано:** передатчик ни разу не включался. Весь тракт проверен на моделях и на приёме, но не в эфире.
|
**Что ещё не сделано:** два независимых устройства пока не разведены. Все передачи выполнены в петле одного PlutoSDR, где передатчик и приёмник делят опорный генератор, поэтому рассогласования несущей и частоты дискретизации между сторонами почти нет.
|
||||||
|
|
||||||
**Ближайшая работа:** Lab042 — передача JPEG через кабельную петлю на одном PlutoSDR. Техническое задание: [docs/lab042_hardware_loopback_spec.md](docs/lab042_hardware_loopback_spec.md).
|
**Ближайшая работа:** Lab043 — передача с PlutoSDR на приём RTL-SDR. Общий опорный генератор исчезает, впервые появляется реальное рассогласование двух устройств. Техническое задание на завершённую Lab042: [docs/lab042_hardware_loopback_spec.md](docs/lab042_hardware_loopback_spec.md).
|
||||||
|
|
||||||
Хронология решений и результатов: [PROJECT_LOG.md](PROJECT_LOG.md).
|
Хронология решений и результатов: [PROJECT_LOG.md](PROJECT_LOG.md).
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@@ -1,11 +1,11 @@
|
|||||||
index,packet_bytes,frame_bits,correlation_score,sample_phase,carrier_offset_hz,carrier_coherence,cfo_correction_applied,error_vector_magnitude,frame_found,header_parsed,crc_valid,fragment_recovered
|
index,packet_bytes,frame_bits,correlation_score,sample_phase,carrier_offset_hz,carrier_coherence,cfo_correction_applied,error_vector_magnitude,frame_found,header_parsed,crc_valid,fragment_recovered
|
||||||
0,536,4384,0.9995083676316812,15,-0.8161804653898344,0.9999521460387033,False,0.013939054015288657,True,True,True,True
|
0,536,4384,0.9991557547061216,19,-0.13990840791652204,0.9997935741467082,False,0.3400949142192574,True,True,True,True
|
||||||
1,536,4384,0.9994247234007716,49,0.034058565997241885,0.9999449532988285,False,0.007533311365459869,True,True,True,True
|
1,536,4384,0.9986269709852709,51,-0.6704420071784511,0.999254107740255,False,0.1962803864132724,True,True,True,True
|
||||||
2,536,4384,0.9995067920215722,121,-0.4024654150100275,0.9999698768639075,False,0.007802389734928202,True,True,True,True
|
2,536,4384,0.9988503105697645,75,0.16739056727394086,0.9997145042457541,False,0.16392107712020013,True,True,True,True
|
||||||
3,536,4384,0.9995973624847003,79,0.05360429263743072,0.9999563681724133,False,0.008067411996806204,True,True,True,True
|
3,536,4384,0.9984613689137739,69,0.12541328438695132,0.9993958830571172,False,0.18678168516977972,True,True,True,True
|
||||||
4,536,4384,0.9995573203179389,37,-0.7726047795897841,0.999951969069221,False,0.008830283803028178,True,True,True,True
|
4,536,4384,0.9986165925127464,37,0.522370456657967,0.9996364225258064,False,0.15291774582164716,True,True,True,True
|
||||||
5,536,4384,0.9994972026358666,35,0.14518673176235097,0.9999740428064059,False,0.007829934600060385,True,True,True,True
|
5,536,4384,0.9988383991200348,32,-0.1940305233482036,0.9995177607884775,False,0.1768680391093208,True,True,True,True
|
||||||
6,536,4384,0.9994993201100448,33,-0.6575975246787777,0.9999581020309138,False,0.008192298206454165,True,True,True,True
|
6,536,4384,0.9993940824417119,51,0.35917365385569155,0.9997260503953309,False,0.1442403116319193,True,True,True,True
|
||||||
7,536,4384,0.9995337736352732,97,-0.32146831121434616,0.9999654759558527,False,0.00773812855638587,True,True,True,True
|
7,536,4384,0.9986343538268945,75,0.2939902862233339,0.9996675003381007,False,0.17829502789805413,True,True,True,True
|
||||||
8,536,4384,0.9995377136427829,90,-0.32692415971800415,0.9999706666958124,False,0.008197030505111991,True,True,True,True
|
8,536,4384,0.9988608071024292,69,-0.7492708746413825,0.9995548745704333,False,0.15854008568562075,True,True,True,True
|
||||||
9,172,1472,0.9994478378475372,53,-0.6175162038574342,0.9999730687021144,False,0.011683449034017782,True,True,True,True
|
9,172,1472,0.9984590339381546,60,0.5753674284248689,0.9992453603986811,False,0.232235822035248,True,True,True,True
|
||||||
|
|||||||
|
@@ -1,13 +1,11 @@
|
|||||||
Lab042 — передача JPEG через кабельную петлю на одном PlutoSDR
|
Lab042 — передача JPEG через кабельную петлю на одном PlutoSDR
|
||||||
|
|
||||||
Режим прогона
|
Режим прогона
|
||||||
- Канал: air.
|
- Канал: cable.
|
||||||
- Трасса: две антенны по воздуху, расстояние 60 см, аттенюатора нет.
|
- Трасса: кабель через аттенюатор 30 дБ.
|
||||||
- Несущая 225.000 МГц — внутри целевого диапазона 200-250 МГц, закреплённого Записью 018.
|
- Несущая 435.000 МГц, одинаковая на передачу и приём. Сигнал в эфир не выходит, поэтому её значение произвольно.
|
||||||
- Усиление передатчика -10.0 дБ, приёмника 50.0 дБ, режим ручной.
|
- Усиление передатчика -10.0 дБ, приёмника 20.0 дБ, режим ручной.
|
||||||
- Антенны разнесены на 60 см. Аттенюатора в цепи нет: приёмник защищён ослаблением на трассе.
|
- Антенны не подключались: они создали бы путь распространения параллельно кабельному.
|
||||||
- Использованы штыри, рассчитанные на 108 МГц. На рабочей частоте они работают как полуволновые, согласование плохое, но для этой дистанции достаточное.
|
|
||||||
- Уровни в эфирной трассе не контролируются так же строго, как в кабельной: возможны переотражения от предметов в помещении. Прогон служит проверкой тракта устройства, а не измерением.
|
|
||||||
|
|
||||||
Параметры тракта
|
Параметры тракта
|
||||||
- Символьная скорость 20000 симв/с, отсчётов на символ 128.
|
- Символьная скорость 20000 симв/с, отсчётов на символ 128.
|
||||||
@@ -33,28 +31,28 @@ Lab042 — передача JPEG через кабельную петлю на
|
|||||||
- Размер принятого изображения 4756 байт.
|
- Размер принятого изображения 4756 байт.
|
||||||
|
|
||||||
Качество сигнала
|
Качество сигнала
|
||||||
- Средняя нормированная корреляция при поиске кадра 0.99951.
|
- Средняя нормированная корреляция при поиске кадра 0.99879.
|
||||||
- Средняя оценка ухода несущей -0.368 Гц.
|
- Средняя оценка ухода несущей 0.029 Гц.
|
||||||
- Средняя величина вектора ошибки 0.898%.
|
- Средняя величина вектора ошибки 19.302%.
|
||||||
|
|
||||||
Скорость
|
Скорость
|
||||||
- Длительность сигнала в эфире 2.067 с.
|
- Длительность сигнала в эфире 2.067 с.
|
||||||
- Канальная скорость 18.404 кбит/с — полезные байты, делённые на время эфира.
|
- Канальная скорость 18.404 кбит/с — полезные байты, делённые на время эфира.
|
||||||
- Полное время работы программы 16.638 с, что даёт 2.287 кбит/с.
|
- Полное время работы программы 16.648 с, что даёт 2.285 кбит/с.
|
||||||
- В программном режиме вторая величина отражает быстродействие вычислений, а не пропускную способность канала.
|
- В программном режиме вторая величина отражает быстродействие вычислений, а не пропускную способность канала.
|
||||||
|
|
||||||
Кадры
|
Кадры
|
||||||
номер | корреляция | фаза | уход несущей, Гц | достоверность оценки | вектор ошибки, % | найден | заголовок | CRC32 | фрагмент
|
номер | корреляция | фаза | уход несущей, Гц | достоверность оценки | вектор ошибки, % | найден | заголовок | CRC32 | фрагмент
|
||||||
0 | 0.99951 | 15 | -0.816 | 0.99995 | 1.394 | да | да | да | да
|
0 | 0.99916 | 19 | -0.140 | 0.99979 | 34.009 | да | да | да | да
|
||||||
1 | 0.99942 | 49 | 0.034 | 0.99994 | 0.753 | да | да | да | да
|
1 | 0.99863 | 51 | -0.670 | 0.99925 | 19.628 | да | да | да | да
|
||||||
2 | 0.99951 | 121 | -0.402 | 0.99997 | 0.780 | да | да | да | да
|
2 | 0.99885 | 75 | 0.167 | 0.99971 | 16.392 | да | да | да | да
|
||||||
3 | 0.99960 | 79 | 0.054 | 0.99996 | 0.807 | да | да | да | да
|
3 | 0.99846 | 69 | 0.125 | 0.99940 | 18.678 | да | да | да | да
|
||||||
4 | 0.99956 | 37 | -0.773 | 0.99995 | 0.883 | да | да | да | да
|
4 | 0.99862 | 37 | 0.522 | 0.99964 | 15.292 | да | да | да | да
|
||||||
5 | 0.99950 | 35 | 0.145 | 0.99997 | 0.783 | да | да | да | да
|
5 | 0.99884 | 32 | -0.194 | 0.99952 | 17.687 | да | да | да | да
|
||||||
6 | 0.99950 | 33 | -0.658 | 0.99996 | 0.819 | да | да | да | да
|
6 | 0.99939 | 51 | 0.359 | 0.99973 | 14.424 | да | да | да | да
|
||||||
7 | 0.99953 | 97 | -0.321 | 0.99997 | 0.774 | да | да | да | да
|
7 | 0.99863 | 75 | 0.294 | 0.99967 | 17.830 | да | да | да | да
|
||||||
8 | 0.99954 | 90 | -0.327 | 0.99997 | 0.820 | да | да | да | да
|
8 | 0.99886 | 69 | -0.749 | 0.99955 | 15.854 | да | да | да | да
|
||||||
9 | 0.99945 | 53 | -0.618 | 0.99997 | 1.168 | да | да | да | да
|
9 | 0.99846 | 60 | 0.575 | 0.99925 | 23.224 | да | да | да | да
|
||||||
|
|
||||||
Функциональные проверки
|
Функциональные проверки
|
||||||
- ПРОЙДЕНО — 01. Размер подготовленного JPEG: JPEG занимает 4756 байт при пределе 5000, качество 30
|
- ПРОЙДЕНО — 01. Размер подготовленного JPEG: JPEG занимает 4756 байт при пределе 5000, качество 30
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 67 KiB |
@@ -1,2 +1,2 @@
|
|||||||
loopback_mode,path_description,antenna_distance_cm,source_image,image_width,image_height,jpeg_quality,jpeg_bytes,fragment_data_size,fragment_count,packet_bytes_max,packet_bytes_min,frame_bits_max,frame_bits_min,symbol_rate,samples_per_symbol,sample_rate_hz,carrier_hz,transmitted_samples,air_time_seconds,frames_received,frames_failed_crc,fragments_recovered,fragment_delivery_fraction,image_reassembled,image_identical,received_image_bytes,transfer_seconds,channel_bitrate_kbps,wall_clock_bitrate_kbps,mean_correlation_score,mean_carrier_offset_hz,mean_error_vector_magnitude,tx_gain_db,rx_gain_db,attenuator_db
|
loopback_mode,path_description,antenna_distance_cm,source_image,image_width,image_height,jpeg_quality,jpeg_bytes,fragment_data_size,fragment_count,packet_bytes_max,packet_bytes_min,frame_bits_max,frame_bits_min,symbol_rate,samples_per_symbol,sample_rate_hz,carrier_hz,transmitted_samples,air_time_seconds,frames_received,frames_failed_crc,fragments_recovered,fragment_delivery_fraction,image_reassembled,image_identical,received_image_bytes,transfer_seconds,channel_bitrate_kbps,wall_clock_bitrate_kbps,mean_correlation_score,mean_carrier_offset_hz,mean_error_vector_magnitude,tx_gain_db,rx_gain_db,attenuator_db
|
||||||
air,"две антенны по воздуху, расстояние 60 см, аттенюатора нет",60.0,data/raw/lab011_scene.jpg,320,180,30,4756,512,10,536,172,4384,1472,20000,128,2560000,225000000,5292544,2.0674,10,0,10,1.0,True,True,4756,16.637855200009653,18.40379220276676,2.286833221146072,0.9995110413728169,-0.3681907269061185,0.00898132918175413,-10.0,50.0,0.0
|
cable,кабель через аттенюатор 30 дБ,0.0,data/raw/lab011_scene.jpg,320,180,30,4756,512,10,536,172,4384,1472,20000,128,2560000,435000000,5292544,2.0674,10,0,10,1.0,True,True,4756,16.64845450001303,18.40379220276676,2.285377300335609,0.9987897674116901,0.02900538637381942,0.193017509510432,-10.0,20.0,30.0
|
||||||
|
|||||||
|
@@ -527,22 +527,31 @@ def _failed_result(plan: FramePlan, correlation: float = 0.0, phase: int = 0) ->
|
|||||||
def _error_vector_magnitude(received: np.ndarray, reference: np.ndarray) -> float:
|
def _error_vector_magnitude(received: np.ndarray, reference: np.ndarray) -> float:
|
||||||
"""Среднеквадратичное отклонение принятых символов от переданных.
|
"""Среднеквадратичное отклонение принятых символов от переданных.
|
||||||
|
|
||||||
Нормируется на энергию переданных символов, поэтому не зависит от
|
Нормируется на мощность подогнанного сигнала, то есть на мощность
|
||||||
общего усиления тракта.
|
масштабированного эталона scale*reference, а не на единичный эталон.
|
||||||
|
Это принципиально: эталон здесь — единичные BPSK-символы мощностью 1,
|
||||||
|
и нормировка на него делала метрику тем меньше, чем слабее сигнал,
|
||||||
|
вплоть до почти нуля при непрошедшей контрольной сумме. Нормировка на
|
||||||
|
мощность подогнанного сигнала сохраняет независимость от общего
|
||||||
|
усиления тракта и при этом растёт с ухудшением отношения сигнал/шум.
|
||||||
|
|
||||||
|
Возвращает NaN, если величину вычислить нельзя: ноль означал бы
|
||||||
|
идеальный приём там, где сигнала нет.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Ноль здесь возвращать нельзя: он означал бы идеальный приём там, где
|
|
||||||
# величину вычислить не удалось. Такое значение уже один раз скрыло
|
|
||||||
# отказ приёмника, показав 0,000% при непрошедшей контрольной сумме.
|
|
||||||
if not len(received) or len(received) != len(reference):
|
if not len(received) or len(received) != len(reference):
|
||||||
return float("nan")
|
return float("nan")
|
||||||
|
|
||||||
scale = np.vdot(reference, received) / np.vdot(reference, reference)
|
reference_energy = np.vdot(reference, reference)
|
||||||
difference = received - scale * reference
|
if reference_energy == 0:
|
||||||
reference_power = float(np.mean(np.abs(reference) ** 2))
|
|
||||||
if reference_power <= 0.0:
|
|
||||||
return float("nan")
|
return float("nan")
|
||||||
return float(np.sqrt(np.mean(np.abs(difference) ** 2) / reference_power))
|
|
||||||
|
scale = np.vdot(reference, received) / reference_energy
|
||||||
|
difference = received - scale * reference
|
||||||
|
signal_power = float(np.abs(scale) ** 2 * np.mean(np.abs(reference) ** 2))
|
||||||
|
if signal_power <= 0.0:
|
||||||
|
return float("nan")
|
||||||
|
return float(np.sqrt(np.mean(np.abs(difference) ** 2) / signal_power))
|
||||||
|
|
||||||
|
|
||||||
# ==================================================================== прогон
|
# ==================================================================== прогон
|
||||||
@@ -886,6 +895,39 @@ def save_plots(metrics: TransferMetrics, results: tuple[FrameResult, ...], segme
|
|||||||
plt.close(figure)
|
plt.close(figure)
|
||||||
|
|
||||||
|
|
||||||
|
def created_files(metrics: TransferMetrics) -> tuple[Path, ...]:
|
||||||
|
"""Вернуть файлы, действительно созданные прогоном.
|
||||||
|
|
||||||
|
Принятое изображение попадает в список только если оно собрано:
|
||||||
|
объявлять несуществующий файл созданным нельзя.
|
||||||
|
"""
|
||||||
|
|
||||||
|
paths = [SUMMARY_CSV, FRAMES_CSV, REPORT_PATH, *PLOT_PATHS]
|
||||||
|
if metrics.image_reassembled:
|
||||||
|
paths.insert(0, RECEIVED_IMAGE_PATH)
|
||||||
|
return tuple(paths)
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_meets_criteria(
|
||||||
|
metrics: TransferMetrics,
|
||||||
|
tests: tuple[FunctionalTestResult, ...],
|
||||||
|
) -> bool:
|
||||||
|
"""Проверить, достигнут ли критерий приёмки Lab042.
|
||||||
|
|
||||||
|
Критерий: пройдены все функциональные проверки, изображение собрано
|
||||||
|
побайтово идентично переданному и восстановлены все фрагменты. По этому
|
||||||
|
признаку `main` выбирает код завершения, чтобы автоматика не приняла
|
||||||
|
провал за успех.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
bool(tests)
|
||||||
|
and all(item.passed for item in tests)
|
||||||
|
and metrics.image_identical
|
||||||
|
and metrics.fragments_recovered == metrics.fragment_count
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def write_report(
|
def write_report(
|
||||||
metrics: TransferMetrics,
|
metrics: TransferMetrics,
|
||||||
results: tuple[FrameResult, ...],
|
results: tuple[FrameResult, ...],
|
||||||
@@ -990,10 +1032,7 @@ def write_report(
|
|||||||
"Созданные файлы",
|
"Созданные файлы",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lines.extend(
|
lines.extend(f"- {path.as_posix()}" for path in created_files(metrics))
|
||||||
f"- {path.as_posix()}"
|
|
||||||
for path in (RECEIVED_IMAGE_PATH, SUMMARY_CSV, FRAMES_CSV, REPORT_PATH, *PLOT_PATHS)
|
|
||||||
)
|
|
||||||
|
|
||||||
REPORT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
REPORT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
@@ -1025,7 +1064,7 @@ def validate_outputs(metrics: TransferMetrics, results: tuple[FrameResult, ...],
|
|||||||
assert not any(path.suffix.lower() in forbidden for path in actual)
|
assert not any(path.suffix.lower() in forbidden for path in actual)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Lab042: передача JPEG через кабельную петлю")
|
parser = argparse.ArgumentParser(description="Lab042: передача JPEG через кабельную петлю")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--loopback",
|
"--loopback",
|
||||||
@@ -1078,13 +1117,20 @@ def main() -> None:
|
|||||||
write_report(metrics, results, tests)
|
write_report(metrics, results, tests)
|
||||||
validate_outputs(metrics, results, tests)
|
validate_outputs(metrics, results, tests)
|
||||||
|
|
||||||
|
success = transfer_meets_criteria(metrics, tests)
|
||||||
|
passed_tests = sum(item.passed for item in tests)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"Lab042 завершена, режим {metrics.loopback_mode}: "
|
f"Lab042 завершена, режим {metrics.loopback_mode}: "
|
||||||
f"фрагментов {metrics.fragments_recovered}/{metrics.fragment_count}, "
|
f"фрагментов {metrics.fragments_recovered}/{metrics.fragment_count}, "
|
||||||
f"изображение {'идентично' if metrics.image_identical else 'НЕ совпадает'}, "
|
f"изображение {'идентично' if metrics.image_identical else 'НЕ совпадает'}, "
|
||||||
f"{len(tests)}/{len(tests)} проверок, два CSV и четыре PNG"
|
f"{passed_tests}/{len(tests)} проверок, два CSV и четыре PNG"
|
||||||
)
|
)
|
||||||
|
if not success:
|
||||||
|
print("Критерий Lab042 НЕ достигнут: изображение не собрано побайтово. Код завершения ненулевой.")
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
|
|||||||
125
tests/test_lab042_reporting.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""Проверки отчётности Lab042: неуспешный путь не должен маскироваться.
|
||||||
|
|
||||||
|
Три дефекта, найденные при разборе кабельного прогона, закрыты правками в
|
||||||
|
experiments/lab042_pluto_image_loopback.py. Эти тесты фиксируют их, чтобы
|
||||||
|
провал лаборатории нельзя было принять за успех:
|
||||||
|
|
||||||
|
1. EVM не занижается искусственно при слабом сигнале и не превращается в
|
||||||
|
ноль, когда её нельзя вычислить;
|
||||||
|
2. отсутствующее принятое изображение не объявляется созданным файлом;
|
||||||
|
3. невыполнение критерия лаборатории даёт ненулевой код завершения.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from experiments.lab042_pluto_image_loopback import (
|
||||||
|
RECEIVED_IMAGE_PATH,
|
||||||
|
FunctionalTestResult,
|
||||||
|
_error_vector_magnitude,
|
||||||
|
created_files,
|
||||||
|
transfer_meets_criteria,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _reference(length: int = 512) -> np.ndarray:
|
||||||
|
bits = np.random.default_rng(length).integers(0, 2, size=length).astype(np.float64)
|
||||||
|
return (2.0 * bits - 1.0).astype(np.complex128)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ EVM
|
||||||
|
|
||||||
|
|
||||||
|
def test_evm_is_near_zero_for_a_clean_signal() -> None:
|
||||||
|
reference = _reference()
|
||||||
|
assert _error_vector_magnitude(reference.copy(), reference) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_evm_is_invariant_to_overall_gain() -> None:
|
||||||
|
"""Одно и то же отношение сигнал/шум при разной амплитуде — одна EVM."""
|
||||||
|
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
reference = _reference()
|
||||||
|
noise = 0.1 * (rng.standard_normal(len(reference)) + 1j * rng.standard_normal(len(reference)))
|
||||||
|
|
||||||
|
strong = _error_vector_magnitude(reference + noise, reference)
|
||||||
|
weak = _error_vector_magnitude(0.001 * (reference + noise), reference)
|
||||||
|
|
||||||
|
assert math.isclose(strong, weak, rel_tol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_evm_grows_when_the_signal_is_weak_and_noisy() -> None:
|
||||||
|
"""Регрессия: прежняя нормировка на единичный эталон занижала EVM у
|
||||||
|
|
||||||
|
слабого сигнала почти до нуля. Теперь слабый шумный сигнал обязан дать
|
||||||
|
заметную EVM, а не подделку под идеальный приём.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rng = np.random.default_rng(2)
|
||||||
|
reference = _reference()
|
||||||
|
noise = 0.5 * (rng.standard_normal(len(reference)) + 1j * rng.standard_normal(len(reference)))
|
||||||
|
weak_noisy = 0.001 * (reference + noise)
|
||||||
|
|
||||||
|
assert _error_vector_magnitude(weak_noisy, reference) > 0.1
|
||||||
|
|
||||||
|
|
||||||
|
def test_evm_is_not_a_number_when_it_cannot_be_computed() -> None:
|
||||||
|
reference = _reference()
|
||||||
|
assert math.isnan(_error_vector_magnitude(reference[:10], reference))
|
||||||
|
assert math.isnan(_error_vector_magnitude(np.zeros(0, dtype=complex), np.zeros(0, dtype=complex)))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ состав файлов
|
||||||
|
|
||||||
|
|
||||||
|
def _metrics(**overrides) -> SimpleNamespace:
|
||||||
|
base = dict(
|
||||||
|
image_reassembled=True,
|
||||||
|
image_identical=True,
|
||||||
|
fragments_recovered=10,
|
||||||
|
fragment_count=10,
|
||||||
|
)
|
||||||
|
base.update(overrides)
|
||||||
|
return SimpleNamespace(**base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_received_image_listed_only_when_reassembled() -> None:
|
||||||
|
with_image = created_files(_metrics(image_reassembled=True))
|
||||||
|
without_image = created_files(_metrics(image_reassembled=False, image_identical=False))
|
||||||
|
|
||||||
|
assert RECEIVED_IMAGE_PATH in with_image
|
||||||
|
assert RECEIVED_IMAGE_PATH not in without_image
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ критерий и код возврата
|
||||||
|
|
||||||
|
|
||||||
|
def _all_passed() -> tuple[FunctionalTestResult, ...]:
|
||||||
|
return tuple(FunctionalTestResult(f"{index:02d}", True, "ок") for index in range(11))
|
||||||
|
|
||||||
|
|
||||||
|
def test_criteria_met_on_a_clean_transfer() -> None:
|
||||||
|
assert transfer_meets_criteria(_metrics(), _all_passed()) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_criteria_failed_when_image_differs() -> None:
|
||||||
|
assert transfer_meets_criteria(_metrics(image_identical=False), _all_passed()) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_criteria_failed_when_a_fragment_is_missing() -> None:
|
||||||
|
metrics = _metrics(image_identical=False, fragments_recovered=9)
|
||||||
|
assert transfer_meets_criteria(metrics, _all_passed()) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_criteria_failed_when_a_functional_check_failed() -> None:
|
||||||
|
tests = _all_passed()[:-1] + (FunctionalTestResult("bad", False, "провал"),)
|
||||||
|
assert transfer_meets_criteria(_metrics(), tests) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_criteria_failed_without_any_tests() -> None:
|
||||||
|
assert transfer_meets_criteria(_metrics(), ()) is False
|
||||||