diff --git a/PROJECT_LOG.md b/PROJECT_LOG.md index 95fa49d..ac1a812 100644 --- a/PROJECT_LOG.md +++ b/PROJECT_LOG.md @@ -538,3 +538,56 @@ Lab042: первая передача изображения через реал ## Следующий этап Кабельная петля через аттенюатор 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. diff --git a/README.md b/README.md index 10816ee..c27fdf2 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ## 4. Текущее состояние -**Выполнено:** лабораторные Lab001–Lab041. Пройден путь от структуры пакета до сеансов связи с защитой от пакетов предыдущего сеанса. +**Выполнено:** лабораторные Lab001–Lab042. Пройден путь от структуры пакета до первой передачи изображения через реальный радиотракт. Что реализовано и проверено: @@ -45,11 +45,12 @@ - блочное исправление стираний над 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). diff --git a/data/processed/lab042/lab042_constellation.png b/data/processed/lab042/lab042_constellation.png index 0376d9e..88583f6 100644 Binary files a/data/processed/lab042/lab042_constellation.png and b/data/processed/lab042/lab042_constellation.png differ diff --git a/data/processed/lab042/lab042_fragment_delivery.png b/data/processed/lab042/lab042_fragment_delivery.png index 724afc4..92e2d5c 100644 Binary files a/data/processed/lab042/lab042_fragment_delivery.png and b/data/processed/lab042/lab042_fragment_delivery.png differ diff --git a/data/processed/lab042/lab042_frame_correlation.png b/data/processed/lab042/lab042_frame_correlation.png index dab8fc8..ce24278 100644 Binary files a/data/processed/lab042/lab042_frame_correlation.png and b/data/processed/lab042/lab042_frame_correlation.png differ diff --git a/data/processed/lab042/lab042_frames.csv b/data/processed/lab042/lab042_frames.csv index 874f698..8aa0b15 100644 --- a/data/processed/lab042/lab042_frames.csv +++ b/data/processed/lab042/lab042_frames.csv @@ -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 -0,536,4384,0.9995083676316812,15,-0.8161804653898344,0.9999521460387033,False,0.013939054015288657,True,True,True,True -1,536,4384,0.9994247234007716,49,0.034058565997241885,0.9999449532988285,False,0.007533311365459869,True,True,True,True -2,536,4384,0.9995067920215722,121,-0.4024654150100275,0.9999698768639075,False,0.007802389734928202,True,True,True,True -3,536,4384,0.9995973624847003,79,0.05360429263743072,0.9999563681724133,False,0.008067411996806204,True,True,True,True -4,536,4384,0.9995573203179389,37,-0.7726047795897841,0.999951969069221,False,0.008830283803028178,True,True,True,True -5,536,4384,0.9994972026358666,35,0.14518673176235097,0.9999740428064059,False,0.007829934600060385,True,True,True,True -6,536,4384,0.9994993201100448,33,-0.6575975246787777,0.9999581020309138,False,0.008192298206454165,True,True,True,True -7,536,4384,0.9995337736352732,97,-0.32146831121434616,0.9999654759558527,False,0.00773812855638587,True,True,True,True -8,536,4384,0.9995377136427829,90,-0.32692415971800415,0.9999706666958124,False,0.008197030505111991,True,True,True,True -9,172,1472,0.9994478378475372,53,-0.6175162038574342,0.9999730687021144,False,0.011683449034017782,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.9986269709852709,51,-0.6704420071784511,0.999254107740255,False,0.1962803864132724,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.9984613689137739,69,0.12541328438695132,0.9993958830571172,False,0.18678168516977972,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.9988383991200348,32,-0.1940305233482036,0.9995177607884775,False,0.1768680391093208,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.9986343538268945,75,0.2939902862233339,0.9996675003381007,False,0.17829502789805413,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.9984590339381546,60,0.5753674284248689,0.9992453603986811,False,0.232235822035248,True,True,True,True diff --git a/data/processed/lab042/lab042_report.txt b/data/processed/lab042/lab042_report.txt index d48a206..12f164c 100644 --- a/data/processed/lab042/lab042_report.txt +++ b/data/processed/lab042/lab042_report.txt @@ -1,13 +1,11 @@ Lab042 — передача JPEG через кабельную петлю на одном PlutoSDR Режим прогона -- Канал: air. -- Трасса: две антенны по воздуху, расстояние 60 см, аттенюатора нет. -- Несущая 225.000 МГц — внутри целевого диапазона 200-250 МГц, закреплённого Записью 018. -- Усиление передатчика -10.0 дБ, приёмника 50.0 дБ, режим ручной. -- Антенны разнесены на 60 см. Аттенюатора в цепи нет: приёмник защищён ослаблением на трассе. -- Использованы штыри, рассчитанные на 108 МГц. На рабочей частоте они работают как полуволновые, согласование плохое, но для этой дистанции достаточное. -- Уровни в эфирной трассе не контролируются так же строго, как в кабельной: возможны переотражения от предметов в помещении. Прогон служит проверкой тракта устройства, а не измерением. +- Канал: cable. +- Трасса: кабель через аттенюатор 30 дБ. +- Несущая 435.000 МГц, одинаковая на передачу и приём. Сигнал в эфир не выходит, поэтому её значение произвольно. +- Усиление передатчика -10.0 дБ, приёмника 20.0 дБ, режим ручной. +- Антенны не подключались: они создали бы путь распространения параллельно кабельному. Параметры тракта - Символьная скорость 20000 симв/с, отсчётов на символ 128. @@ -33,28 +31,28 @@ Lab042 — передача JPEG через кабельную петлю на - Размер принятого изображения 4756 байт. Качество сигнала -- Средняя нормированная корреляция при поиске кадра 0.99951. -- Средняя оценка ухода несущей -0.368 Гц. -- Средняя величина вектора ошибки 0.898%. +- Средняя нормированная корреляция при поиске кадра 0.99879. +- Средняя оценка ухода несущей 0.029 Гц. +- Средняя величина вектора ошибки 19.302%. Скорость - Длительность сигнала в эфире 2.067 с. - Канальная скорость 18.404 кбит/с — полезные байты, делённые на время эфира. -- Полное время работы программы 16.638 с, что даёт 2.287 кбит/с. +- Полное время работы программы 16.648 с, что даёт 2.285 кбит/с. - В программном режиме вторая величина отражает быстродействие вычислений, а не пропускную способность канала. Кадры номер | корреляция | фаза | уход несущей, Гц | достоверность оценки | вектор ошибки, % | найден | заголовок | CRC32 | фрагмент -0 | 0.99951 | 15 | -0.816 | 0.99995 | 1.394 | да | да | да | да -1 | 0.99942 | 49 | 0.034 | 0.99994 | 0.753 | да | да | да | да -2 | 0.99951 | 121 | -0.402 | 0.99997 | 0.780 | да | да | да | да -3 | 0.99960 | 79 | 0.054 | 0.99996 | 0.807 | да | да | да | да -4 | 0.99956 | 37 | -0.773 | 0.99995 | 0.883 | да | да | да | да -5 | 0.99950 | 35 | 0.145 | 0.99997 | 0.783 | да | да | да | да -6 | 0.99950 | 33 | -0.658 | 0.99996 | 0.819 | да | да | да | да -7 | 0.99953 | 97 | -0.321 | 0.99997 | 0.774 | да | да | да | да -8 | 0.99954 | 90 | -0.327 | 0.99997 | 0.820 | да | да | да | да -9 | 0.99945 | 53 | -0.618 | 0.99997 | 1.168 | да | да | да | да +0 | 0.99916 | 19 | -0.140 | 0.99979 | 34.009 | да | да | да | да +1 | 0.99863 | 51 | -0.670 | 0.99925 | 19.628 | да | да | да | да +2 | 0.99885 | 75 | 0.167 | 0.99971 | 16.392 | да | да | да | да +3 | 0.99846 | 69 | 0.125 | 0.99940 | 18.678 | да | да | да | да +4 | 0.99862 | 37 | 0.522 | 0.99964 | 15.292 | да | да | да | да +5 | 0.99884 | 32 | -0.194 | 0.99952 | 17.687 | да | да | да | да +6 | 0.99939 | 51 | 0.359 | 0.99973 | 14.424 | да | да | да | да +7 | 0.99863 | 75 | 0.294 | 0.99967 | 17.830 | да | да | да | да +8 | 0.99886 | 69 | -0.749 | 0.99955 | 15.854 | да | да | да | да +9 | 0.99846 | 60 | 0.575 | 0.99925 | 23.224 | да | да | да | да Функциональные проверки - ПРОЙДЕНО — 01. Размер подготовленного JPEG: JPEG занимает 4756 байт при пределе 5000, качество 30 diff --git a/data/processed/lab042/lab042_spectrum.png b/data/processed/lab042/lab042_spectrum.png index b6fbb17..86ca3a1 100644 Binary files a/data/processed/lab042/lab042_spectrum.png and b/data/processed/lab042/lab042_spectrum.png differ diff --git a/data/processed/lab042/lab042_summary.csv b/data/processed/lab042/lab042_summary.csv index a0e2790..c774187 100644 --- a/data/processed/lab042/lab042_summary.csv +++ b/data/processed/lab042/lab042_summary.csv @@ -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 -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 diff --git a/experiments/lab042_pluto_image_loopback.py b/experiments/lab042_pluto_image_loopback.py index 9e2c617..341b957 100644 --- a/experiments/lab042_pluto_image_loopback.py +++ b/experiments/lab042_pluto_image_loopback.py @@ -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: """Среднеквадратичное отклонение принятых символов от переданных. - Нормируется на энергию переданных символов, поэтому не зависит от - общего усиления тракта. + Нормируется на мощность подогнанного сигнала, то есть на мощность + масштабированного эталона scale*reference, а не на единичный эталон. + Это принципиально: эталон здесь — единичные BPSK-символы мощностью 1, + и нормировка на него делала метрику тем меньше, чем слабее сигнал, + вплоть до почти нуля при непрошедшей контрольной сумме. Нормировка на + мощность подогнанного сигнала сохраняет независимость от общего + усиления тракта и при этом растёт с ухудшением отношения сигнал/шум. + + Возвращает NaN, если величину вычислить нельзя: ноль означал бы + идеальный приём там, где сигнала нет. """ - # Ноль здесь возвращать нельзя: он означал бы идеальный приём там, где - # величину вычислить не удалось. Такое значение уже один раз скрыло - # отказ приёмника, показав 0,000% при непрошедшей контрольной сумме. if not len(received) or len(received) != len(reference): return float("nan") - scale = np.vdot(reference, received) / np.vdot(reference, reference) - difference = received - scale * reference - reference_power = float(np.mean(np.abs(reference) ** 2)) - if reference_power <= 0.0: + reference_energy = np.vdot(reference, reference) + if reference_energy == 0: 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) +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( metrics: TransferMetrics, results: tuple[FrameResult, ...], @@ -990,10 +1032,7 @@ def write_report( "Созданные файлы", ] ) - lines.extend( - f"- {path.as_posix()}" - for path in (RECEIVED_IMAGE_PATH, SUMMARY_CSV, FRAMES_CSV, REPORT_PATH, *PLOT_PATHS) - ) + lines.extend(f"- {path.as_posix()}" for path in created_files(metrics)) 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) -def main() -> None: +def main() -> int: parser = argparse.ArgumentParser(description="Lab042: передача JPEG через кабельную петлю") parser.add_argument( "--loopback", @@ -1078,13 +1117,20 @@ def main() -> None: write_report(metrics, results, tests) validate_outputs(metrics, results, tests) + success = transfer_meets_criteria(metrics, tests) + passed_tests = sum(item.passed for item in tests) + print( f"Lab042 завершена, режим {metrics.loopback_mode}: " f"фрагментов {metrics.fragments_recovered}/{metrics.fragment_count}, " 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__": - main() + raise SystemExit(main()) diff --git a/tests/test_lab042_reporting.py b/tests/test_lab042_reporting.py new file mode 100644 index 0000000..ecbcb12 --- /dev/null +++ b/tests/test_lab042_reporting.py @@ -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