diff --git a/PROJECT_LOG.md b/PROJECT_LOG.md index a3aa8d2..d08cc36 100644 --- a/PROJECT_LOG.md +++ b/PROJECT_LOG.md @@ -139,3 +139,27 @@ git version 2.51.1.windows.1 - D=2 сохранён как резервный вариант. - D=4 и D=8 исключены из основной линии разработки. - Следующая лабораторная подбирает размер блока и количество проверочных пакетов. + +--- + +# Запись 007 + +## Дата + +29 июля 2026 года + +## Тема + +Завершение Lab032: параметры блочного исправления потерянных пакетов. + +## Выполнено + +- Исследованы параметры блочного исправления потерянных пакетов. +- Сравнивались режимы без исправления, 4+1, 8+2, 12+3, 6+2, 8+3, 4+2 и 8+4. +- Режим 12+3 выбран основным. +- Его транспортный поток составляет 226,951 кбит/с. +- Запас до контрольных 300 кбит/с составляет 73,049 кбит/с. +- Режим 8+4 оставлен усиленным экспериментальным вариантом. +- Режим 8+3 не принят из-за отсутствия устойчивого преимущества. +- Подтверждено, что продолжительные помехи нельзя компенсировать только увеличением числа проверочных пакетов. +- Следующий этап посвящён совместной передаче команд, телеметрии и видео с приоритетным обслуживанием. diff --git a/data/processed/lab032/lab032_block_size_effect.png b/data/processed/lab032/lab032_block_size_effect.png new file mode 100644 index 0000000..636aef3 Binary files /dev/null and b/data/processed/lab032/lab032_block_size_effect.png differ diff --git a/data/processed/lab032/lab032_composite_success.png b/data/processed/lab032/lab032_composite_success.png new file mode 100644 index 0000000..bec09bd Binary files /dev/null and b/data/processed/lab032/lab032_composite_success.png differ diff --git a/data/processed/lab032/lab032_no_image_duration.png b/data/processed/lab032/lab032_no_image_duration.png new file mode 100644 index 0000000..595e428 Binary files /dev/null and b/data/processed/lab032/lab032_no_image_duration.png differ diff --git a/data/processed/lab032/lab032_practical_frontier.png b/data/processed/lab032/lab032_practical_frontier.png new file mode 100644 index 0000000..245e32f Binary files /dev/null and b/data/processed/lab032/lab032_practical_frontier.png differ diff --git a/data/processed/lab032/lab032_publication_delay.png b/data/processed/lab032/lab032_publication_delay.png new file mode 100644 index 0000000..4ac6e01 Binary files /dev/null and b/data/processed/lab032/lab032_publication_delay.png differ diff --git a/data/processed/lab032/lab032_report.txt b/data/processed/lab032/lab032_report.txt new file mode 100644 index 0000000..ac854fc --- /dev/null +++ b/data/processed/lab032/lab032_report.txt @@ -0,0 +1,101 @@ +Lab032. Подбор параметров блочного исправления потерь + +Исходный профиль и неизменные слои +- Видео: data\raw\lab026_rover_source.mp4 +- 63 реальных пар BASE/ROI, 3 fps; длительность 20.766667 с. +- BASE 240x135 grayscale JPEG Q23; ROI 320x180 grayscale JPEG Q33. +- Внутренний Lab028 packet: payload 512 байт; внешний формат и GF(256) Lab030 не изменены. +- Глубина D=1: systematic symbols блока передаются первыми, затем parity; перемежение отсутствует. +- Блоки формируются из непрерывного потока внутренних пакетов и могут пересекать composite_frame_id. + +Последний неполный блок +- Используется единое правило: + r_last = max(1, ceil(k_last × r / k)). +- Нулевое дополнение существует только внутри математики GF(256); внешние пакеты синтетически не добавляются. + +Сравнительная таблица восьми режимов +mode | rate | outer/margin kbit/s | composite 10/50/200/1000 ms | pub p95 200 ms s | no-image p95 200 ms s | block mean/max ms | decode mean ms | queue mean/max +----:|-----:|----------------------:|----------------------------:|-------------------:|------------------------:|------------------:|---------------:|---------------: +Без FEC | 1.000000 | 168.396/131.604 | 0.669603/0.914524/0.960714/0.979762 | 0.203013 | 1.325184 | 0.000/0.000 | 0.000 | 13.717/15 +4+1 | 0.800000 | 226.951/73.049 | 0.791429/0.909048/0.959683/0.978175 | 0.274880 | 1.337167 | 92.966/189.840 | 77.606 | 17.171/20 +8+2 | 0.800000 | 226.951/73.049 | 0.912857/0.933333/0.963016/0.978333 | 0.275067 | 1.338747 | 189.723/266.720 | 159.003 | 17.210/21 +12+3 | 0.800000 | 226.951/73.049 | 0.951984/0.951190/0.965714/0.978492 | 0.288773 | 1.339135 | 288.795/337.173 | 242.715 | 17.219/21 +6+2 | 0.750000 | 242.928/57.072 | 0.920873/0.930317/0.961270/0.978254 | 0.294080 | 1.333927 | 142.659/214.293 | 111.939 | 18.337/21 +8+3 | 0.727273 | 250.916/49.084 | 0.963413/0.948810/0.962540/0.978175 | 0.305947 | 1.339067 | 191.857/266.720 | 145.777 | 18.985/24 +4+2 | 0.666667 | 274.659/25.341 | 0.924762/0.924127/0.959048/0.977143 | 0.336320 | 1.349667 | 96.393/159.120 | 65.744 | 20.698/24 +8+4 | 0.666667 | 274.659/25.341 | 0.986984/0.955317/0.963095/0.978175 | 0.336667 | 1.347973 | 193.876/266.720 | 132.578 | 20.859/26 + +Подробные результаты при средней Bad 200 мс +mode | BASE/ROI/composite | blocks ok/fail | restored packets | delivered kbit/s | publication mean/p95/max s | no-image mean/p95/max s | incomplete mean/p95/max +----:|-------------------:|---------------:|-----------------:|-----------------:|--------------------------:|-----------------------:|------------------------: +Без FEC | 0.972302/0.967302/0.960714 | 0.000000/0.000000 | 0 | 150.881 | 0.185098/0.203013/0.209680 | 0.786259/1.325184/2.333307 | 1.387/3.000/6 +4+1 | 0.973175/0.966905/0.959683 | 0.975972/0.024028 | 92 | 150.067 | 0.246589/0.274880/0.394773 | 0.823033/1.337167/2.349520 | 1.494/3.000/6 +8+2 | 0.974841/0.969365/0.963016 | 0.972130/0.027870 | 144 | 150.483 | 0.246330/0.275067/0.466747 | 0.837464/1.338747/2.334160 | 1.528/3.000/6 +12+3 | 0.975794/0.971270/0.965714 | 0.968403/0.031597 | 260 | 150.899 | 0.246565/0.288773/0.522907 | 0.841596/1.339135/2.380240 | 1.543/3.000/6 +6+2 | 0.973651/0.968571/0.961270 | 0.975243/0.024757 | 188 | 150.202 | 0.262330/0.294080/0.425493 | 0.839718/1.333927/2.364880 | 1.544/3.000/6 +8+3 | 0.974603/0.969524/0.962540 | 0.973981/0.026019 | 249 | 150.081 | 0.271028/0.305947/0.466747 | 0.861515/1.339067/2.334160 | 1.589/3.000/6 +4+2 | 0.973095/0.966429/0.959048 | 0.978287/0.021713 | 236 | 149.535 | 0.296583/0.336320/0.394773 | 0.865525/1.349667/2.364880 | 1.613/3.000/6 +8+4 | 0.975238/0.970238/0.963095 | 0.975833/0.024167 | 375 | 149.944 | 0.295920/0.336667/0.465947 | 0.899563/1.347973/2.334160 | 1.703/3.000/6 + +Временная модель и очередь +- Непрерывные чередующиеся экспоненциальные Good/Bad интервалы Lab029B, средняя доля Bad около 2%. +- Средние Bad 10, 50, 200 и 1000 мс; 200 повторов; fixed seeds 300300...300303. +- Пакет теряется при любом пересечении передачи с Bad. FIFO учитывает фактическую длину каждого внешнего пакета и скорость 300 кбит/с. +- Все исследованные режимы остаются ниже 300 кбит/с; положительный запас показан в таблице. Отдельная функциональная проверка ускоряет поток 8+4 вдвое и подтверждает реальное накопление очереди при перегрузке. +- Block duration измеряется от начала первого до конца последнего символа. Decode-ready без потерь — от начала первого до конца k-го systematic symbol. + +1. Одинаковая кодовая скорость 0.8: 4+1, 8+2 и 12+3 +- 4+1: outer 226.951 кбит/с, block mean 92.966 мс, decode-ready 77.606 мс, composite@200 0.959683. +- 8+2: outer 226.951 кбит/с, block mean 189.723 мс, decode-ready 159.003 мс, composite@200 0.963016. +- 12+3: outer 226.951 кбит/с, block mean 288.795 мс, decode-ready 242.715 мс, composite@200 0.965714. +- При одинаковой номинальной code rate длина блока меняет временное окно, в котором одна помеха может собрать несколько стираний. Короткий блок раньше получает k systematic symbols и быстрее допускает декодирование. +- Длинный блок дольше остаётся открытым и может включить больше потерянных пакетов одной длительной помехи; при этом его больший абсолютный r иногда полезен для рассеянных стираний. + +2. Одинаковая code rate 2/3: 4+2 и 8+4 +- 4+2: outer 274.659 кбит/с, composite@200 0.959048, publication P95 0.336320 с, no-image P95 1.349667 с. +- 8+4: outer 274.659 кбит/с, composite@200 0.963095, publication P95 0.336667 с, no-image P95 1.347973 с. +- Сравнение изолирует влияние длины блока при одинаковой сильной избыточности: короткий 4+2 быстрее закрывается, длинный 8+4 имеет больше абсолютных parity symbols, но дольше накапливает стирания. + +3. Влияние увеличения r +- Большее r позволяет исправить больше стираний, но увеличивает внешний поток, служебную долю, время передачи блока и публикационную задержку. +- 8+3: outer 250.916 кбит/с (8+2 226.951, 8+4 274.659); composite@200 0.962540 (8+2 0.963016, 8+4 0.963095); P95 delay 0.305947 с (8+2 0.275067, 8+4 0.336667). +- Поэтому 8+3 оценивается по фактическому положению между 8+2 и 8+4 сразу по скорости, устойчивости и задержке, без автоматического назначения рабочим режимом. + +4. Практическая граница без автоматического выбора +- Минимальный поток: Без FEC, 168.396 кбит/с. +- Максимальная composite-устойчивость при Bad 200 мс: 12+3, 0.965714. +- Минимальная P95-задержка среди защищённых режимов: 4+1, 0.274880 с. +- Недоминируемые по outer bitrate↓, publication P95↓ и composite success↑ режимы при Bad 200 мс: Без FEC, 8+2, 12+3. +- Это набор компромиссов, а не автоматический выбор единственного режима. + +Функциональные проверки +- PASS all_k_r_supported: GF(256) generated every requested k+r matrix +- PASS recover_any_r_erasures: recovered all 1208 exact-r erasure patterns +- PASS restored_inner_byte_exact: restored Lab028 packets are byte-exact +- PASS inner_outer_crc: outer and recovered Lab028 CRC32 checks passed +- PASS final_partial_block_scaling: final blocks: 4+1:2+1, 8+2:6+2, 12+3:10+3, 6+2:4+2, 8+3:6+3, 4+2:2+1, 8+4:6+3 +- PASS zero_bad_100_percent: all eight modes restored 100% of frames without Bad +- PASS fixed_seed_reproducibility: identical seed produced identical result fields +- PASS queue_counts_parity: every parity packet is included in FIFO timing +- PASS overload_grows_queue: 549.317 kbit/s produced queue mean/max 301.733/589 +- PASS atomic_incomplete_composite: missing final ROI fragment prevented publication +- PASS lab030_8_2_reproduction: 8+2 exactly reproduced Lab030 order and four rows + +Допущения модели +- JPEG кодируются один раз в памяти; вычислительная задержка GF(256) считается нулевой. +- Стирается весь внешний пакет; отдельные битовые ошибки не моделируются, но оба уровня CRC проверяются. +- Publication delay считается от frame_id/3 до конца пакета, завершившего атомарную сборку BASE+ROI. +- No-image заканчивается публикацией следующего полного кадра или концом расписания; неполный кадр не публикуется. +- ARQ, повторы, глубокое перемежение, команды, телеметрия, модуляция и реальный SDR отсутствуют. + +Артефакты +- Полный CSV: data\processed\lab032\lab032_results.csv +- Сводный CSV: data\processed\lab032\lab032_summary.csv +- Отчёт: data\processed\lab032\lab032_report.txt +- Доля восстановленных кадров: data\processed\lab032\lab032_composite_success.png +- Скорость и избыточность: data\processed\lab032\lab032_stream_overhead.png +- Задержка публикации: data\processed\lab032\lab032_publication_delay.png +- Отсутствие изображения: data\processed\lab032\lab032_no_image_duration.png +- Влияние размера блока: data\processed\lab032\lab032_block_size_effect.png +- Практическая граница: data\processed\lab032\lab032_practical_frontier.png +- JPEG, пакеты и бинарные дампы не сохранялись. diff --git a/data/processed/lab032/lab032_results.csv b/data/processed/lab032/lab032_results.csv new file mode 100644 index 0000000..231a84e --- /dev/null +++ b/data/processed/lab032/lab032_results.csv @@ -0,0 +1,33 @@ +mode,label,source_block_size,nominal_parity_count,code_rate,mean_bad_duration_ms,mean_good_duration_ms,target_bad_time_fraction,actual_bad_time_fraction,monte_carlo_repetitions,seed,source_jpeg_bitrate_kbps,inner_packet_stream_bitrate_kbps,outer_fec_stream_bitrate_kbps,margin_to_control_bitrate_kbps,service_and_parity_percent,control_stream_bitrate_kbps,schedule_duration_seconds,fec_block_count,final_block_source_count,final_block_parity_count,mean_block_duration_seconds,max_block_duration_seconds,mean_decode_ready_duration_seconds,mean_queue_length_packets,max_queue_length_packets,transmitted_source_packets,transmitted_parity_packets,lost_source_packets,lost_parity_packets,recovered_source_packets,fec_recovered_blocks,fec_unrecoverable_blocks,fec_affected_block_recovery_rate,fec_all_block_success_rate,fec_unrecoverable_block_rate,base_objects_completed,base_success_rate,roi_objects_completed,roi_success_rate,atomic_composite_frames_completed,composite_success_rate,base_only_frames,roi_only_frames,incomplete_frames,effective_delivered_video_bitrate_kbps,mean_publication_delay_seconds,p95_publication_delay_seconds,max_publication_delay_seconds,mean_no_new_image_duration_seconds,p95_no_new_image_duration_seconds,max_no_new_image_duration_seconds,mean_consecutive_incomplete_frames,p95_consecutive_incomplete_frames,max_consecutive_incomplete_frames +none,Без FEC,0,0,1.0,10.0,490.0,0.02,0.020237233778310743,200,300300,157.76975922953451,168.39601926163724,168.39601926163724,131.60398073836276,6.3102798265039075,300.0,20.869679999999992,0,0,0,0.0,0.0,0.0,13.7169373549884,15,172400,0,8130,0,0,0,0,0.0,0.0,0.0,10778,0.8553968253968254,9644,0.7653968253968254,8437,0.6696031746031746,2341,1207,4163,105.09054858531616,0.18497208960530362,0.20301333333332394,0.20967999999999998,0.8136578817966899,1.3455733333333306,3.0084000000000004,1.476241134751773,3.0,8 +4+1,4+1,4,1,0.8,10.0,490.0,0.02,0.020205583457443846,200,300300,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.95928000000002,216,2,1,0.09296604938271637,0.18984000000000556,0.07760604938271627,17.17068645640074,20,172400,43200,8454,2166,2948,2948,2885,0.5054003085890623,0.9332175925925926,0.0667824074074074,11517,0.914047619047619,10717,0.8505555555555555,9972,0.7914285714285715,1545,745,2628,123.60095384955959,0.25078923118064045,0.277253333333352,0.3947733333333403,0.7504291108955716,1.0431573333333346,2.687546666666666,1.274490785645005,2.0,7 +8+2,8+2,8,2,0.8,10.0,490.0,0.02,0.02054934259858452,200,300300,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,108,6,2,0.18972345679012387,0.2667200000000012,0.15900345679012368,17.209647495361782,21,172400,43200,8451,2229,5560,4070,1028,0.798352295017654,0.9524074074074074,0.047592592592592596,12102,0.9604761904761905,11797,0.9362698412698413,11502,0.9128571428571428,600,295,1098,142.6072647730782,0.2601807152379309,0.39477333333332965,0.46559999999999846,0.7307525925925931,1.0170573333333313,1.7975999999999885,1.196078431372549,2.0,4 +12+3,12+3,12,3,0.8,10.0,490.0,0.02,0.02054934259858452,200,300300,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,72,10,3,0.2887951851851859,0.33717333333333244,0.24271518518518562,17.21892393320965,21,172400,43200,8382,2249,6633,4195,481,0.8971343028229256,0.9665972222222222,0.03340277777777778,12298,0.976031746031746,12141,0.9635714285714285,11995,0.9519841269841269,303,146,605,148.739525445967,0.27203066166458373,0.43695999999999735,0.5433600000000043,0.7779739823008857,1.130100000000001,1.8652533333333245,1.3384955752212389,2.0,4 +6+2,6+2,6,2,0.75,10.0,490.0,0.02,0.02054934259858452,200,300300,157.76975922953451,168.39601926163724,242.92776886035313,57.072231139646874,35.05486837932128,300.0,20.974640000000022,144,4,2,0.14265870370370404,0.21429333333333744,0.11193870370370387,18.337391304347825,21,172400,57600,8406,2992,5902,4396,959,0.8209150326797385,0.9667013888888889,0.03329861111111111,12165,0.9654761904761905,11882,0.943015873015873,11603,0.9208730158730158,562,279,997,143.8502954043548,0.27183035421873797,0.3794133333333327,0.4374400000000094,0.7170305679592641,1.015386666666668,1.4916266666666544,1.171562867215041,2.0,3 +8+3,8+3,8,3,0.7272727272727273,10.0,490.0,0.02,0.020402467996494884,200,300300,157.76975922953451,168.39601926163724,250.91595505617977,49.08404494382023,37.12246828058022,300.0,21.020720000000026,108,6,3,0.19185679012345724,0.2667200000000012,0.145776790123457,18.984822934232714,24,172400,64800,8524,3269,7148,4741,412,0.9200465748107899,0.980925925925926,0.019074074074074073,12377,0.9823015873015873,12259,0.972936507936508,12139,0.9634126984126984,238,120,461,150.1392511769338,0.28472546338248755,0.410133333333329,0.48210666666667734,0.7119441206030163,1.0108000000000015,1.6533066666666691,1.1582914572864322,2.0,4 +4+2,4+2,4,2,0.6666666666666666,10.0,490.0,0.02,0.020402467996494884,200,300300,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.020720000000026,216,2,1,0.09639320987654372,0.1591200000000068,0.06574432098765466,20.698375870069604,24,172400,86200,8475,4327,6317,4860,936,0.8385093167701864,0.9783333333333334,0.021666666666666667,12181,0.9667460317460318,11940,0.9476190476190476,11652,0.9247619047619048,529,288,948,144.11669438534915,0.30090820688866216,0.34253333333332137,0.41933333333331824,0.6944152068696341,1.0002666666666684,1.6533066666666691,1.1100702576112411,2.0,4 +8+4,8+4,8,4,0.6666666666666666,10.0,490.0,0.02,0.019902265840707405,200,300300,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.051440000000028,108,6,3,0.1938755555555563,0.2667200000000012,0.13257777777777818,20.85924207269915,26,172400,86200,8364,4311,7797,4860,144,0.9712230215827338,0.9933333333333333,0.006666666666666667,12520,0.9936507936507937,12472,0.9898412698412699,12436,0.986984126984127,84,36,164,153.61973147680135,0.3084344440870609,0.4101333333333308,0.4963199999999981,0.7097399061032867,1.0169693333333332,1.0686133333333285,1.1549295774647887,2.0,2 +none,Без FEC,0,0,1.0,50.0,2450.0,0.02,0.019137425732142044,200,300301,157.76975922953451,168.39601926163724,168.39601926163724,131.60398073836276,6.3102798265039075,300.0,20.869679999999992,0,0,0,0.0,0.0,0.0,13.7169373549884,15,172400,0,4062,0,0,0,0,0.0,0.0,0.0,12052,0.9565079365079365,11819,0.938015873015873,11523,0.9145238095238095,529,296,1077,143.55281729283826,0.18500322485463208,0.20301333333332394,0.20967999999999998,0.6934565404298871,0.9983146666666661,1.6871733333333334,1.1023541453428864,2.0,4 +4+1,4+1,4,1,0.8,50.0,2450.0,0.02,0.020366888699952315,200,300301,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.95928000000002,216,2,1,0.09296604938271637,0.18984000000000556,0.07760604938271627,17.17068645640074,20,172400,43200,4404,1111,435,435,1555,0.2185929648241206,0.9640046296296296,0.03599537037037037,12072,0.9580952380952381,11759,0.9332539682539682,11454,0.909047619047619,618,305,1146,142.09415018073125,0.2470573493975917,0.27522666666668627,0.394773333333335,0.714345825703629,1.0041866666666657,1.993706666666668,1.165818921668362,2.0,5 +8+2,8+2,8,2,0.8,50.0,2450.0,0.02,0.019947721301082785,200,300301,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,108,6,2,0.18972345679012387,0.2667200000000012,0.15900345679012368,17.209647495361782,21,172400,43200,4326,1104,971,658,905,0.4209852847088932,0.9581018518518518,0.04189814814814815,12183,0.9669047619047619,11974,0.9503174603174603,11760,0.9333333333333333,423,214,840,145.76717025894112,0.24829524943310788,0.27522666666668627,0.46530666666666143,0.7125767765567773,1.009596,1.6315466666666705,1.1538461538461537,2.0,4 +12+3,12+3,12,3,0.8,50.0,2450.0,0.02,0.019947721301082785,200,300301,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,72,10,3,0.2887951851851859,0.33717333333333244,0.24271518518518562,17.21892393320965,21,172400,43200,4368,1082,1492,799,598,0.5719398711524696,0.9584722222222222,0.041527777777777775,12273,0.974047619047619,12114,0.9614285714285714,11985,0.9511904761904761,288,129,615,148.55436660653038,0.25108506744541914,0.2925866666666561,0.5421066666666583,0.7189982697201026,1.0041866666666657,1.993706666666668,1.1736641221374047,2.0,5 +6+2,6+2,6,2,0.75,50.0,2450.0,0.02,0.019947721301082785,200,300301,157.76975922953451,168.39601926163724,242.92776886035313,57.072231139646874,35.05486837932128,300.0,20.974640000000022,144,4,2,0.14265870370370404,0.21429333333333744,0.11193870370370387,18.337391304347825,21,172400,57600,4368,1480,994,687,973,0.413855421686747,0.9662152777777778,0.03378472222222222,12172,0.966031746031746,11935,0.9472222222222222,11722,0.9303174603174603,450,213,878,145.30510368711916,0.2636889040550546,0.2962933333333524,0.4374400000000094,0.7177993752788941,1.0114399999999997,1.993706666666668,1.1753681392235609,2.0,5 +8+3,8+3,8,3,0.7272727272727273,50.0,2450.0,0.02,0.019374042379375574,200,300301,157.76975922953451,168.39601926163724,250.91595505617977,49.08404494382023,37.12246828058022,300.0,21.020720000000026,108,6,3,0.19185679012345724,0.2667200000000012,0.145776790123457,18.984822934232714,24,172400,64800,4293,1651,1567,863,658,0.5673898750821827,0.9695370370370371,0.030462962962962963,12280,0.9746031746031746,12107,0.9608730158730159,11955,0.9488095238095238,325,152,645,147.8507529713538,0.2735714953297101,0.3079733333333543,0.4809599999999983,0.7145212903225816,1.0080079999999976,1.3905066666666706,1.1559139784946237,2.0,3 +4+2,4+2,4,2,0.6666666666666666,50.0,2450.0,0.02,0.019374042379375574,200,300301,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.020720000000026,216,2,1,0.09639320987654372,0.1591200000000068,0.06574432098765466,20.698375870069604,24,172400,86200,4328,2165,1095,760,1111,0.4061998931052913,0.9742824074074075,0.025717592592592594,12133,0.962936507936508,11923,0.9462698412698413,11644,0.9241269841269841,489,279,956,144.01249053315,0.2971720302301636,0.3366666666666909,0.41933333333331824,0.7232546932006642,1.0119066666666576,1.7137333333333764,1.1890547263681592,2.0,4 +8+4,8+4,8,4,0.6666666666666666,50.0,2450.0,0.02,0.0203533945388834,200,300301,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.051440000000028,108,6,3,0.1938755555555563,0.2667200000000012,0.13257777777777818,20.85924207269915,26,172400,86200,4506,2275,2062,979,520,0.6531020680453635,0.975925925925926,0.024074074074074074,12298,0.976031746031746,12178,0.9665079365079365,12037,0.9553174603174603,261,141,563,148.6380960162343,0.29828562377115203,0.3386933333333566,0.49543999999999855,0.7492293986636983,1.0571200000000047,1.4058666666666717,1.2538975501113585,2.0,3 +none,Без FEC,0,0,1.0,200.0,9800.0,0.02,0.02117165511678711,200,300302,157.76975922953451,168.39601926163724,168.39601926163724,131.60398073836276,6.3102798265039075,300.0,20.869679999999992,0,0,0,0.0,0.0,0.0,13.7169373549884,15,172400,0,3877,0,0,0,0,0.0,0.0,0.0,12251,0.9723015873015873,12188,0.9673015873015873,12105,0.9607142857142857,146,83,495,150.88131681942423,0.18509811593005004,0.20301333333332394,0.20967999999999998,0.7862588982259566,1.3251839999999993,2.333306666666667,1.3865546218487395,3.0,6 +4+1,4+1,4,1,0.8,200.0,9800.0,0.02,0.01946372101257088,200,300302,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.95928000000002,216,2,1,0.09296604938271637,0.18984000000000556,0.07760604938271627,17.17068645640074,20,172400,43200,3597,915,92,92,1038,0.08141592920353982,0.9759722222222222,0.024027777777777776,12262,0.9731746031746031,12183,0.9669047619047619,12092,0.9596825396825397,170,91,508,150.0674164379691,0.24658850369390367,0.27488000000001733,0.394773333333335,0.8230332549019609,1.3371666666666668,2.34952,1.4941176470588236,3.0,6 +8+2,8+2,8,2,0.8,200.0,9800.0,0.02,0.01945312904270856,200,300302,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,108,6,2,0.18972345679012387,0.2667200000000012,0.15900345679012368,17.209647495361782,21,172400,43200,3573,908,144,101,602,0.14366998577524892,0.9721296296296297,0.027870370370370372,12283,0.9748412698412698,12214,0.9693650793650793,12134,0.963015873015873,149,80,466,150.4825408207243,0.2463296192516908,0.27506666666666346,0.4667466666666762,0.8374642185792353,1.3387466666666654,2.3341600000000007,1.5278688524590165,3.0,6 +12+3,12+3,12,3,0.8,200.0,9800.0,0.02,0.01945312904270856,200,300302,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,72,10,3,0.2887951851851859,0.33717333333333244,0.24271518518518562,17.21892393320965,21,172400,43200,3579,903,260,131,455,0.2235494880546075,0.9684027777777777,0.03159722222222222,12295,0.9757936507936508,12238,0.9712698412698413,12168,0.9657142857142857,127,70,432,150.89903235526316,0.2465654372123616,0.2887733333333302,0.5229066666666595,0.8415958095238097,1.3391346666666688,2.380239999999999,1.542857142857143,3.0,6 +6+2,6+2,6,2,0.75,200.0,9800.0,0.02,0.01945312904270856,200,300302,157.76975922953451,168.39601926163724,242.92776886035313,57.072231139646874,35.05486837932128,300.0,20.974640000000022,144,4,2,0.14265870370370404,0.21429333333333744,0.11193870370370387,18.337391304347825,21,172400,57600,3609,1180,188,127,713,0.15119047619047618,0.9752430555555556,0.024756944444444446,12268,0.9736507936507937,12204,0.9685714285714285,12112,0.9612698412698413,156,92,488,150.2020516204329,0.2623295794804066,0.29408000000000545,0.42549333333333905,0.8397175527426163,1.3339266666666698,2.3648799999999994,1.5443037974683544,3.0,6 +8+3,8+3,8,3,0.7272727272727273,200.0,9800.0,0.02,0.01942144600871695,200,300302,157.76975922953451,168.39601926163724,250.91595505617977,49.08404494382023,37.12246828058022,300.0,21.020720000000026,108,6,3,0.19185679012345724,0.2667200000000012,0.145776790123457,18.984822934232714,24,172400,64800,3566,1346,249,130,562,0.18786127167630057,0.9739814814814814,0.026018518518518517,12280,0.9746031746031746,12216,0.9695238095238096,12128,0.9625396825396826,152,88,472,150.08097914819265,0.2710280233069496,0.30594666666668857,0.4667466666666762,0.8615145230078568,1.3390666666666675,2.3341600000000007,1.5892255892255893,3.0,6 +4+2,4+2,4,2,0.6666666666666666,200.0,9800.0,0.02,0.01942144600871695,200,300302,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.020720000000026,216,2,1,0.09639320987654372,0.1591200000000068,0.06574432098765466,20.698375870069604,24,172400,86200,3572,1806,236,159,938,0.14494074749316319,0.978287037037037,0.02171296296296296,12261,0.9730952380952381,12177,0.9664285714285714,12084,0.959047619047619,177,93,516,149.53471051419723,0.29658307403729667,0.33632000000002193,0.39477333333332965,0.8655246666666663,1.349666666666665,2.3648799999999994,1.6125,3.0,6 +8+4,8+4,8,4,0.6666666666666666,200.0,9800.0,0.02,0.019400401043555997,200,300302,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.051440000000028,108,6,3,0.1938755555555563,0.2667200000000012,0.13257777777777818,20.85924207269915,26,172400,86200,3550,1777,375,160,522,0.23460410557184752,0.9758333333333333,0.024166666666666666,12288,0.9752380952380952,12225,0.9702380952380952,12135,0.9630952380952381,153,90,465,149.94388982416385,0.2959204021425649,0.3366666666666909,0.46594666666666207,0.8995634676434676,1.347973333333331,2.3341600000000007,1.7032967032967032,3.0,6 +none,Без FEC,0,0,1.0,1000.0,49000.0,0.02,0.017221990003688604,200,300303,157.76975922953451,168.39601926163724,168.39601926163724,131.60398073836276,6.3102798265039075,300.0,20.869679999999992,0,0,0,0.0,0.0,0.0,13.7169373549884,15,172400,0,3014,0,0,0,0,0.0,0.0,0.0,12372,0.981904761904762,12355,0.9805555555555555,12345,0.9797619047619047,27,10,255,153.8087579684979,0.1850217642770288,0.20301333333332394,0.20967999999999998,1.4030256410256405,3.0499439999999973,4.3269600000000015,3.269230769230769,8.149999999999991,12 +4+1,4+1,4,1,0.8,1000.0,49000.0,0.02,0.01762954307387838,200,300303,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.95928000000002,216,2,1,0.09296604938271637,0.18984000000000556,0.07760604938271627,17.17068645640074,20,172400,43200,3089,774,18,18,806,0.021844660194174758,0.9813425925925926,0.018657407407407407,12362,0.9811111111111112,12340,0.9793650793650793,12325,0.9781746031746031,37,15,275,152.8935249684148,0.24643575388776331,0.27488000000001733,0.37941333333333027,1.444222551440331,3.016560000000001,4.3107466666666685,3.3950617283950617,8.0,12 +8+2,8+2,8,2,0.8,1000.0,49000.0,0.02,0.017631278990127012,200,300303,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,108,6,2,0.18972345679012387,0.2667200000000012,0.15900345679012368,17.209647495361782,21,172400,43200,3079,781,25,16,427,0.03611738148984198,0.9802314814814815,0.01976851851851852,12366,0.9814285714285714,12342,0.9795238095238096,12327,0.9783333333333334,39,15,273,152.80692302704583,0.24594955030961974,0.27506666666666346,0.44994666666666205,1.466306160337555,3.0479386666666684,4.295386666666669,3.4556962025316458,8.099999999999994,12 +12+3,12+3,12,3,0.8,1000.0,49000.0,0.02,0.017631278990127012,200,300303,157.76975922953451,168.39601926163724,226.95139646869984,73.04860353130016,30.4830189704105,300.0,20.974640000000022,72,10,3,0.2887951851851859,0.33717333333333244,0.24271518518518562,17.21892393320965,21,172400,43200,3090,775,34,18,304,0.055900621118012424,0.9788888888888889,0.021111111111111112,12367,0.9815079365079366,12343,0.9796031746031746,12329,0.9784920634920635,38,14,271,152.83293157832492,0.24574732095060553,0.2850666666666548,0.5122933333333357,1.5037301754385983,3.0950066666666745,4.3368533333333215,3.5657894736842106,8.25,12 +6+2,6+2,6,2,0.75,1000.0,49000.0,0.02,0.017631278990127012,200,300303,157.76975922953451,168.39601926163724,242.92776886035313,57.072231139646874,35.05486837932128,300.0,20.974640000000022,144,4,2,0.14265870370370404,0.21429333333333744,0.11193870370370387,18.337391304347825,21,172400,57600,3092,1033,40,25,547,0.043706293706293704,0.9810069444444445,0.018993055555555555,12366,0.9814285714285714,12338,0.9792063492063492,12326,0.9782539682539683,40,12,274,152.79328179172546,0.26206411812429153,0.29408000000000545,0.4192266666666633,1.4673545991561199,3.047938666666669,4.336853333333321,3.4683544303797467,8.099999999999994,12 +8+3,8+3,8,3,0.7272727272727273,1000.0,49000.0,0.02,0.017636471517506424,200,300303,157.76975922953451,168.39601926163724,250.91595505617977,49.08404494382023,37.12246828058022,300.0,21.020720000000026,108,6,3,0.19185679012345724,0.2667200000000012,0.145776790123457,18.984822934232714,24,172400,64800,3080,1165,48,25,418,0.056433408577878104,0.9806481481481482,0.019351851851851853,12368,0.9815873015873016,12340,0.9793650793650793,12325,0.9781746031746031,43,15,275,152.44826628203012,0.27059416362407174,0.3057866666666631,0.44994666666666205,1.508938181818184,3.079317333333341,4.290773333333321,3.5714285714285716,8.200000000000003,12 +4+2,4+2,4,2,0.6666666666666666,1000.0,49000.0,0.02,0.017636471517506424,200,300303,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.020720000000026,216,2,1,0.09639320987654372,0.1591200000000068,0.06574432098765466,20.698375870069604,24,172400,86200,3094,1545,55,36,784,0.04390243902439024,0.9818518518518519,0.01814814814814815,12362,0.9811111111111112,12331,0.9786507936507937,12312,0.9771428571428571,50,19,288,152.2895466948799,0.2964339592809204,0.33632000000002193,0.39477333333333053,1.5150906666666693,3.0350866666667184,4.336853333333319,3.6,8.049999999999997,12 +8+4,8+4,8,4,0.6666666666666666,1000.0,49000.0,0.02,0.017189734334432376,200,300303,157.76975922953451,168.39601926163724,274.6586195826645,25.341380417335472,42.55787075997801,300.0,21.051440000000028,108,6,3,0.1938755555555563,0.2667200000000012,0.13257777777777818,20.85924207269915,26,172400,86200,3004,1509,64,27,400,0.06323185011709602,0.9814814814814815,0.018518518518518517,12372,0.981904761904762,12346,0.9798412698412698,12325,0.9781746031746031,47,21,275,152.2330861926783,0.29551052981744624,0.33650666666666273,0.46469333333333185,1.5328961403508796,3.0950066666666753,4.2754133333333195,3.6184210526315788,8.25,12 diff --git a/data/processed/lab032/lab032_stream_overhead.png b/data/processed/lab032/lab032_stream_overhead.png new file mode 100644 index 0000000..7b957ba Binary files /dev/null and b/data/processed/lab032/lab032_stream_overhead.png differ diff --git a/data/processed/lab032/lab032_summary.csv b/data/processed/lab032/lab032_summary.csv new file mode 100644 index 0000000..f251689 --- /dev/null +++ b/data/processed/lab032/lab032_summary.csv @@ -0,0 +1,9 @@ +mode,label,source_block_size,nominal_parity_count,code_rate,outer_fec_stream_bitrate_kbps,margin_to_control_bitrate_kbps,service_and_parity_percent,fec_block_count,mean_block_duration_seconds,max_block_duration_seconds,mean_decode_ready_duration_seconds,mean_queue_length_packets,max_queue_length_packets,composite_success_10ms,p95_publication_delay_10ms_seconds,p95_no_new_image_10ms_seconds,composite_success_50ms,p95_publication_delay_50ms_seconds,p95_no_new_image_50ms_seconds,composite_success_200ms,p95_publication_delay_200ms_seconds,p95_no_new_image_200ms_seconds,composite_success_1000ms,p95_publication_delay_1000ms_seconds,p95_no_new_image_1000ms_seconds +none,Без FEC,0,0,1.0,168.39601926163724,131.60398073836276,6.3102798265039075,0,0.0,0.0,0.0,13.7169373549884,15,0.6696031746031746,0.20301333333332394,1.3455733333333306,0.9145238095238095,0.20301333333332394,0.9983146666666661,0.9607142857142857,0.20301333333332394,1.3251839999999993,0.9797619047619047,0.20301333333332394,3.0499439999999973 +4+1,4+1,4,1,0.8,226.95139646869984,73.04860353130016,30.4830189704105,216,0.09296604938271637,0.18984000000000556,0.07760604938271627,17.17068645640074,20,0.7914285714285715,0.277253333333352,1.0431573333333346,0.909047619047619,0.27522666666668627,1.0041866666666657,0.9596825396825397,0.27488000000001733,1.3371666666666668,0.9781746031746031,0.27488000000001733,3.016560000000001 +8+2,8+2,8,2,0.8,226.95139646869984,73.04860353130016,30.4830189704105,108,0.18972345679012387,0.2667200000000012,0.15900345679012368,17.209647495361782,21,0.9128571428571428,0.39477333333332965,1.0170573333333313,0.9333333333333333,0.27522666666668627,1.009596,0.963015873015873,0.27506666666666346,1.3387466666666654,0.9783333333333334,0.27506666666666346,3.0479386666666684 +12+3,12+3,12,3,0.8,226.95139646869984,73.04860353130016,30.4830189704105,72,0.2887951851851859,0.33717333333333244,0.24271518518518562,17.21892393320965,21,0.9519841269841269,0.43695999999999735,1.130100000000001,0.9511904761904761,0.2925866666666561,1.0041866666666657,0.9657142857142857,0.2887733333333302,1.3391346666666688,0.9784920634920635,0.2850666666666548,3.0950066666666745 +6+2,6+2,6,2,0.75,242.92776886035313,57.072231139646874,35.05486837932128,144,0.14265870370370404,0.21429333333333744,0.11193870370370387,18.337391304347825,21,0.9208730158730158,0.3794133333333327,1.015386666666668,0.9303174603174603,0.2962933333333524,1.0114399999999997,0.9612698412698413,0.29408000000000545,1.3339266666666698,0.9782539682539683,0.29408000000000545,3.047938666666669 +8+3,8+3,8,3,0.7272727272727273,250.91595505617977,49.08404494382023,37.12246828058022,108,0.19185679012345724,0.2667200000000012,0.145776790123457,18.984822934232714,24,0.9634126984126984,0.410133333333329,1.0108000000000015,0.9488095238095238,0.3079733333333543,1.0080079999999976,0.9625396825396826,0.30594666666668857,1.3390666666666675,0.9781746031746031,0.3057866666666631,3.079317333333341 +4+2,4+2,4,2,0.6666666666666666,274.6586195826645,25.341380417335472,42.55787075997801,216,0.09639320987654372,0.1591200000000068,0.06574432098765466,20.698375870069604,24,0.9247619047619048,0.34253333333332137,1.0002666666666684,0.9241269841269841,0.3366666666666909,1.0119066666666576,0.959047619047619,0.33632000000002193,1.349666666666665,0.9771428571428571,0.33632000000002193,3.0350866666667184 +8+4,8+4,8,4,0.6666666666666666,274.6586195826645,25.341380417335472,42.55787075997801,108,0.1938755555555563,0.2667200000000012,0.13257777777777818,20.85924207269915,26,0.986984126984127,0.4101333333333308,1.0169693333333332,0.9553174603174603,0.3386933333333566,1.0571200000000047,0.9630952380952381,0.3366666666666909,1.347973333333331,0.9781746031746031,0.33650666666666273,3.0950066666666753 diff --git a/tests/lab032_fec_parameter_sweep.py b/tests/lab032_fec_parameter_sweep.py new file mode 100644 index 0000000..59a4777 --- /dev/null +++ b/tests/lab032_fec_parameter_sweep.py @@ -0,0 +1,1786 @@ +""" +Lab032. Parameter sweep for packet-erasure FEC without interleaving. + +The Lab028 512-byte-payload transport, Lab030 outer packet and GF(256) +implementation, and Lab029B continuous-time impairment model are reused +unchanged. Eight fixed modes compare block size and parity count at D=1. +Systematic symbols precede parity symbols and blocks may cross composite-frame +boundaries. No ARQ, retransmission, or automatic mode selection is used. +""" + +from __future__ import annotations + +import csv +from dataclasses import asdict, dataclass +from itertools import combinations +from math import ceil +from pathlib import Path +from typing import Callable + +import cv2 +import matplotlib +import numpy as np + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +from protocol.packet_erasure_fec import ( + decode_fec_block, + decode_outer_symbol, + encode_fec_block, + systematic_generator_matrix, +) +from protocol.video_packet import ( + CompositeReassembler, + decode_packet as decode_inner_packet, +) +from tests.lab028_video_packetization import ( + COMPOSITE_FPS, + SOURCE_VIDEO_PATH, + EncodedComposite, + VideoMetadata, + load_video_profile, +) +from tests.lab029_packet_channel_simulation import ( + PreparedProfile, + prepare_profiles, +) +from tests.lab029b_time_based_burst_simulation import ( + BAD_TIME_FRACTION, + CONTROL_STREAM_BITRATE_BPS, + CONTROL_STREAM_BITRATE_KBPS, + MEAN_BAD_DURATIONS_SECONDS, + generate_bad_intervals, + percentile, +) +from tests.lab030_packet_erasure_fec import ( + FECBlockPlan, + FECMode, + FunctionalTestResult, + ModeSchedule, + RepetitionResult, + SourcePacket, + TransmissionUnit, + build_mode_units as build_lab030_mode_units, + overlap_loss_flags, + prepare_source_packets, + schedule_units, + simulate_baseline, + simulate_fec, +) + + +OUTPUT_DIRECTORY = Path("data/processed/lab032") +CSV_PATH = OUTPUT_DIRECTORY / "lab032_results.csv" +SUMMARY_CSV_PATH = OUTPUT_DIRECTORY / "lab032_summary.csv" +REPORT_PATH = OUTPUT_DIRECTORY / "lab032_report.txt" +COMPOSITE_SUCCESS_PLOT_PATH = ( + OUTPUT_DIRECTORY / "lab032_composite_success.png" +) +STREAM_OVERHEAD_PLOT_PATH = ( + OUTPUT_DIRECTORY / "lab032_stream_overhead.png" +) +PUBLICATION_DELAY_PLOT_PATH = ( + OUTPUT_DIRECTORY / "lab032_publication_delay.png" +) +NO_IMAGE_PLOT_PATH = OUTPUT_DIRECTORY / "lab032_no_image_duration.png" +BLOCK_SIZE_PLOT_PATH = OUTPUT_DIRECTORY / "lab032_block_size_effect.png" +PRACTICAL_FRONTIER_PLOT_PATH = ( + OUTPUT_DIRECTORY / "lab032_practical_frontier.png" +) + +INNER_PAYLOAD_SIZE = 512 +MONTE_CARLO_REPETITIONS = 200 +MASTER_SEED = 300_300 +SEED_BASE = MASTER_SEED + + +@dataclass(frozen=True) +class SweepMode: + name: str + source_count: int + parity_count: int + label: str + + @property + def code_rate(self) -> float: + if self.parity_count == 0: + return 1.0 + return self.source_count / ( + self.source_count + self.parity_count + ) + + +MODES = ( + SweepMode("none", 0, 0, "Без FEC"), + SweepMode("4+1", 4, 1, "4+1"), + SweepMode("8+2", 8, 2, "8+2"), + SweepMode("12+3", 12, 3, "12+3"), + SweepMode("6+2", 6, 2, "6+2"), + SweepMode("8+3", 8, 3, "8+3"), + SweepMode("4+2", 4, 2, "4+2"), + SweepMode("8+4", 8, 4, "8+4"), +) + + +@dataclass(frozen=True) +class ParameterSchedule: + mode: SweepMode + fec_schedule: ModeSchedule + block_durations_seconds: tuple[float, ...] + decode_ready_durations_seconds: tuple[float, ...] + mean_block_duration_seconds: float + max_block_duration_seconds: float + mean_decode_ready_duration_seconds: float + + +@dataclass(frozen=True) +class SimulationResult: + mode: str + label: str + source_block_size: int + nominal_parity_count: int + code_rate: float + mean_bad_duration_ms: float + mean_good_duration_ms: float + target_bad_time_fraction: float + actual_bad_time_fraction: float + monte_carlo_repetitions: int + seed: int + source_jpeg_bitrate_kbps: float + inner_packet_stream_bitrate_kbps: float + outer_fec_stream_bitrate_kbps: float + margin_to_control_bitrate_kbps: float + service_and_parity_percent: float + control_stream_bitrate_kbps: float + schedule_duration_seconds: float + fec_block_count: int + final_block_source_count: int + final_block_parity_count: int + mean_block_duration_seconds: float + max_block_duration_seconds: float + mean_decode_ready_duration_seconds: float + mean_queue_length_packets: float + max_queue_length_packets: int + transmitted_source_packets: int + transmitted_parity_packets: int + lost_source_packets: int + lost_parity_packets: int + recovered_source_packets: int + fec_recovered_blocks: int + fec_unrecoverable_blocks: int + fec_affected_block_recovery_rate: float + fec_all_block_success_rate: float + fec_unrecoverable_block_rate: float + base_objects_completed: int + base_success_rate: float + roi_objects_completed: int + roi_success_rate: float + atomic_composite_frames_completed: int + composite_success_rate: float + base_only_frames: int + roi_only_frames: int + incomplete_frames: int + effective_delivered_video_bitrate_kbps: float + mean_publication_delay_seconds: float + p95_publication_delay_seconds: float + max_publication_delay_seconds: float + mean_no_new_image_duration_seconds: float + p95_no_new_image_duration_seconds: float + max_no_new_image_duration_seconds: float + mean_consecutive_incomplete_frames: float + p95_consecutive_incomplete_frames: float + max_consecutive_incomplete_frames: int + + +CSV_FIELDS = [ + field.name + for field in SimulationResult.__dataclass_fields__.values() +] + + +def parity_for_partial_block( + source_count: int, + nominal_source_count: int, + nominal_parity_count: int, +) -> int: + """Scale parity for a non-empty final block.""" + + if not 1 <= source_count <= nominal_source_count: + raise ValueError("partial source count is outside 1...k") + if nominal_parity_count < 1: + raise ValueError("nominal parity count must be positive") + return max( + 1, + ceil( + source_count + * nominal_parity_count + / nominal_source_count + ), + ) + + +def build_parameter_units( + source_packets: tuple[SourcePacket, ...], + mode: SweepMode, +) -> tuple[ + tuple[TransmissionUnit, ...], + tuple[FECBlockPlan, ...], +]: + """Build non-interleaved systematic-then-parity block units.""" + + if mode.parity_count == 0: + units = tuple( + TransmissionUnit( + sequence_index=index, + generation_time_seconds=packet.generation_time_seconds, + wire_packet=packet.inner_packet, + is_parity=False, + block_id=index, + symbol_index=0, + source_global_index=index, + ) + for index, packet in enumerate(source_packets) + ) + return units, () + + units = [] + blocks = [] + sequence_index = 0 + for block_id, start in enumerate( + range(0, len(source_packets), mode.source_count) + ): + block_sources = source_packets[ + start:start + mode.source_count + ] + source_count = len(block_sources) + parity_count = ( + mode.parity_count + if source_count == mode.source_count + else parity_for_partial_block( + source_count, + mode.source_count, + mode.parity_count, + ) + ) + outer_packets = encode_fec_block( + tuple( + packet.inner_packet for packet in block_sources + ), + block_id, + parity_count, + ) + parsed = tuple( + decode_outer_symbol(packet) + for packet in outer_packets + ) + generation_complete = max( + packet.generation_time_seconds + for packet in block_sources + ) + blocks.append( + FECBlockPlan( + block_id=block_id, + source_global_indices=tuple( + packet.global_index + for packet in block_sources + ), + source_count=source_count, + parity_count=parity_count, + symbol_size=parsed[0].symbol_size, + ) + ) + for local_index, (wire_packet, symbol) in enumerate( + zip(outer_packets, parsed) + ): + is_parity = symbol.is_parity + units.append( + TransmissionUnit( + sequence_index=sequence_index, + generation_time_seconds=( + generation_complete + if is_parity + else block_sources[ + local_index + ].generation_time_seconds + ), + wire_packet=wire_packet, + is_parity=is_parity, + block_id=block_id, + symbol_index=symbol.symbol_index, + source_global_index=( + None + if is_parity + else block_sources[ + local_index + ].global_index + ), + ) + ) + sequence_index += 1 + return tuple(units), tuple(blocks) + + +def block_timing_metrics( + schedule: ModeSchedule, +) -> tuple[tuple[float, ...], tuple[float, ...]]: + if not schedule.blocks: + return (), () + + scheduled_by_block = { + plan.block_id: [] for plan in schedule.blocks + } + plans = {plan.block_id: plan for plan in schedule.blocks} + for unit in schedule.units: + scheduled_by_block[unit.unit.block_id].append(unit) + + block_durations = [] + decode_ready = [] + for block_id, units in scheduled_by_block.items(): + ordered = sorted( + units, key=lambda item: item.unit.symbol_index + ) + first_start = ordered[0].start_seconds + block_durations.append( + ordered[-1].end_seconds - first_start + ) + source_count = plans[block_id].source_count + kth_source = next( + unit + for unit in ordered + if unit.unit.symbol_index == source_count - 1 + ) + decode_ready.append( + kth_source.end_seconds - first_start + ) + return tuple(block_durations), tuple(decode_ready) + + +def build_schedules( + metadata: VideoMetadata, + composites: list[EncodedComposite], + profile: PreparedProfile, +) -> dict[str, ParameterSchedule]: + source_packets = prepare_source_packets(profile) + total_jpeg_bytes = sum( + len(composite.base_jpeg) + len(composite.roi_jpeg) + for composite in composites + ) + total_inner_bytes = sum( + len(packet.inner_packet) for packet in source_packets + ) + schedules = {} + for mode in MODES: + units, blocks = build_parameter_units( + source_packets, mode + ) + scheduled, duration, mean_queue, max_queue = schedule_units( + units + ) + fec_schedule = ModeSchedule( + mode=FECMode( + mode.name, mode.parity_count, mode.label + ), + source_packets=source_packets, + blocks=blocks, + units=scheduled, + source_duration_seconds=metadata.duration_seconds, + duration_seconds=duration, + total_jpeg_bytes=total_jpeg_bytes, + total_inner_bytes=total_inner_bytes, + total_transmitted_bytes=sum( + len(unit.unit.wire_packet) + for unit in scheduled + ), + mean_queue_length_packets=mean_queue, + max_queue_length_packets=max_queue, + ) + durations, decode_ready = block_timing_metrics( + fec_schedule + ) + schedules[mode.name] = ParameterSchedule( + mode=mode, + fec_schedule=fec_schedule, + block_durations_seconds=durations, + decode_ready_durations_seconds=decode_ready, + mean_block_duration_seconds=( + float(np.mean(durations)) + if durations + else 0.0 + ), + max_block_duration_seconds=( + max(durations) if durations else 0.0 + ), + mean_decode_ready_duration_seconds=( + float(np.mean(decode_ready)) + if decode_ready + else 0.0 + ), + ) + return schedules + + +def simulate_condition( + schedule: ParameterSchedule, + frame_count: int, + mean_bad_duration_seconds: float, + seed: int, + repetitions: int, +) -> SimulationResult: + fec_schedule = schedule.fec_schedule + rng = np.random.default_rng(seed) + repetition_results: list[RepetitionResult] = [] + bad_time_seconds = 0.0 + for _ in range(repetitions): + intervals = generate_bad_intervals( + fec_schedule.duration_seconds, + mean_bad_duration_seconds, + rng, + ) + bad_time_seconds += sum( + interval.duration_seconds for interval in intervals + ) + loss_flags = overlap_loss_flags(fec_schedule, intervals) + repetition_results.append( + simulate_baseline( + fec_schedule, loss_flags, frame_count + ) + if schedule.mode.parity_count == 0 + else simulate_fec( + fec_schedule, loss_flags, frame_count + ) + ) + + def total(field: str) -> int: + return sum( + int(getattr(result, field)) + for result in repetition_results + ) + + def flattened(field: str) -> list[float]: + return [ + float(value) + for result in repetition_results + for value in getattr(result, field) + ] + + publication_delays = flattened("publication_delays") + no_image = flattened("no_new_image_durations") + incomplete_runs = flattened("incomplete_frame_runs") + recovered_blocks = total("fec_recovered_blocks") + failed_blocks = total("fec_unrecoverable_blocks") + affected_blocks = recovered_blocks + failed_blocks + total_frames = frame_count * repetitions + total_fec_blocks = len(fec_schedule.blocks) * repetitions + parity_packets_per_pass = sum( + int(unit.unit.is_parity) for unit in fec_schedule.units + ) + source_rate = ( + fec_schedule.total_jpeg_bytes + * 8.0 + / fec_schedule.source_duration_seconds + / 1000.0 + ) + inner_rate = ( + fec_schedule.total_inner_bytes + * 8.0 + / fec_schedule.source_duration_seconds + / 1000.0 + ) + outer_rate = ( + fec_schedule.total_transmitted_bytes + * 8.0 + / fec_schedule.source_duration_seconds + / 1000.0 + ) + final_plan = ( + fec_schedule.blocks[-1] + if fec_schedule.blocks + else None + ) + return SimulationResult( + mode=schedule.mode.name, + label=schedule.mode.label, + source_block_size=schedule.mode.source_count, + nominal_parity_count=schedule.mode.parity_count, + code_rate=schedule.mode.code_rate, + mean_bad_duration_ms=mean_bad_duration_seconds * 1000.0, + mean_good_duration_ms=( + mean_bad_duration_seconds + * (1.0 - BAD_TIME_FRACTION) + / BAD_TIME_FRACTION + * 1000.0 + ), + target_bad_time_fraction=BAD_TIME_FRACTION, + actual_bad_time_fraction=( + bad_time_seconds + / (fec_schedule.duration_seconds * repetitions) + ), + monte_carlo_repetitions=repetitions, + seed=seed, + source_jpeg_bitrate_kbps=source_rate, + inner_packet_stream_bitrate_kbps=inner_rate, + outer_fec_stream_bitrate_kbps=outer_rate, + margin_to_control_bitrate_kbps=( + CONTROL_STREAM_BITRATE_KBPS - outer_rate + ), + service_and_parity_percent=( + ( + fec_schedule.total_transmitted_bytes + - fec_schedule.total_jpeg_bytes + ) + / fec_schedule.total_transmitted_bytes + * 100.0 + ), + control_stream_bitrate_kbps=CONTROL_STREAM_BITRATE_KBPS, + schedule_duration_seconds=fec_schedule.duration_seconds, + fec_block_count=len(fec_schedule.blocks), + final_block_source_count=( + final_plan.source_count if final_plan else 0 + ), + final_block_parity_count=( + final_plan.parity_count if final_plan else 0 + ), + mean_block_duration_seconds=( + schedule.mean_block_duration_seconds + ), + max_block_duration_seconds=( + schedule.max_block_duration_seconds + ), + mean_decode_ready_duration_seconds=( + schedule.mean_decode_ready_duration_seconds + ), + mean_queue_length_packets=( + fec_schedule.mean_queue_length_packets + ), + max_queue_length_packets=( + fec_schedule.max_queue_length_packets + ), + transmitted_source_packets=( + len(fec_schedule.source_packets) * repetitions + ), + transmitted_parity_packets=( + parity_packets_per_pass * repetitions + ), + lost_source_packets=total("lost_source_packets"), + lost_parity_packets=total("lost_parity_packets"), + recovered_source_packets=total("recovered_source_packets"), + fec_recovered_blocks=recovered_blocks, + fec_unrecoverable_blocks=failed_blocks, + fec_affected_block_recovery_rate=( + recovered_blocks / affected_blocks + if affected_blocks + else 0.0 + ), + fec_all_block_success_rate=( + (total_fec_blocks - failed_blocks) / total_fec_blocks + if total_fec_blocks + else 0.0 + ), + fec_unrecoverable_block_rate=( + failed_blocks / total_fec_blocks + if total_fec_blocks + else 0.0 + ), + base_objects_completed=total("base_objects_completed"), + base_success_rate=( + total("base_objects_completed") / total_frames + ), + roi_objects_completed=total("roi_objects_completed"), + roi_success_rate=( + total("roi_objects_completed") / total_frames + ), + atomic_composite_frames_completed=total( + "atomic_composite_frames_completed" + ), + composite_success_rate=( + total("atomic_composite_frames_completed") + / total_frames + ), + base_only_frames=total("base_only_frames"), + roi_only_frames=total("roi_only_frames"), + incomplete_frames=total("incomplete_frames"), + effective_delivered_video_bitrate_kbps=( + total("delivered_jpeg_bytes") + * 8.0 + / (fec_schedule.duration_seconds * repetitions) + / 1000.0 + ), + mean_publication_delay_seconds=( + float(np.mean(publication_delays)) + if publication_delays + else 0.0 + ), + p95_publication_delay_seconds=percentile( + publication_delays, 95 + ), + max_publication_delay_seconds=( + max(publication_delays) + if publication_delays + else 0.0 + ), + mean_no_new_image_duration_seconds=( + float(np.mean(no_image)) if no_image else 0.0 + ), + p95_no_new_image_duration_seconds=percentile( + no_image, 95 + ), + max_no_new_image_duration_seconds=( + max(no_image) if no_image else 0.0 + ), + mean_consecutive_incomplete_frames=( + float(np.mean(incomplete_runs)) + if incomplete_runs + else 0.0 + ), + p95_consecutive_incomplete_frames=percentile( + incomplete_runs, 95 + ), + max_consecutive_incomplete_frames=( + int(max(incomplete_runs)) + if incomplete_runs + else 0 + ), + ) + + +def run_monte_carlo( + schedules: dict[str, ParameterSchedule], + frame_count: int, +) -> list[SimulationResult]: + results = [] + for duration_index, duration in enumerate( + MEAN_BAD_DURATIONS_SECONDS + ): + seed = SEED_BASE + duration_index + for mode in MODES: + results.append( + simulate_condition( + schedules[mode.name], + frame_count, + duration, + seed, + MONTE_CARLO_REPETITIONS, + ) + ) + return results + + +def result_lookup( + results: list[SimulationResult], + mean_bad_duration_ms: float, +) -> dict[str, SimulationResult]: + return { + result.mode: result + for result in results + if result.mean_bad_duration_ms == mean_bad_duration_ms + } + + +def run_functional_tests( + composites: list[EncodedComposite], + schedules: dict[str, ParameterSchedule], + results: list[SimulationResult], +) -> list[FunctionalTestResult]: + tests: list[tuple[str, Callable[[], str]]] = [] + protected_modes = tuple( + mode for mode in MODES if mode.parity_count + ) + source_packets = schedules[ + "8+2" + ].fec_schedule.source_packets + + def all_k_r_supported() -> str: + for mode in protected_modes: + matrix = systematic_generator_matrix( + mode.source_count, + mode.parity_count, + ) + if len(matrix) != ( + mode.source_count + mode.parity_count + ): + raise AssertionError( + f"bad generator shape for {mode.name}" + ) + return "GF(256) generated every requested k+r matrix" + + def recover_any_r_losses() -> str: + checked = 0 + for mode in protected_modes: + sample = tuple( + packet.inner_packet + for packet in source_packets[ + :mode.source_count + ] + ) + outer = encode_fec_block( + sample, mode.source_count, mode.parity_count + ) + for missing in combinations( + range(len(outer)), mode.parity_count + ): + available = tuple( + packet + for index, packet in enumerate(outer) + if index not in missing + ) + decoded = decode_fec_block(available) + if decoded.source_packets != sample: + raise AssertionError( + f"failed {mode.name}, missing={missing}" + ) + checked += 1 + return f"recovered all {checked} exact-r erasure patterns" + + def restored_inner_is_exact() -> str: + sample = tuple( + packet.inner_packet for packet in source_packets[:12] + ) + outer = encode_fec_block(sample, 77, 3) + available = tuple( + packet + for index, packet in enumerate(outer) + if index not in {0, 6, 14} + ) + decoded = decode_fec_block(available) + if decoded.source_packets != sample: + raise AssertionError("restored inner bytes differ") + return "restored Lab028 packets are byte-exact" + + def crc_layers_pass() -> str: + sample = tuple( + packet.inner_packet for packet in source_packets[:8] + ) + outer = encode_fec_block(sample, 78, 4) + available = tuple( + packet + for index, packet in enumerate(outer) + if index not in {1, 4, 8, 11} + ) + for packet in available: + decode_outer_symbol(packet) + decoded = decode_fec_block(available) + for packet in decoded.source_packets: + decode_inner_packet(packet) + return "outer and recovered Lab028 CRC32 checks passed" + + def final_partial_blocks_are_scaled() -> str: + checked = [] + for mode in protected_modes: + final = schedules[mode.name].fec_schedule.blocks[-1] + expected = parity_for_partial_block( + final.source_count, + mode.source_count, + mode.parity_count, + ) + if final.parity_count != expected: + raise AssertionError( + f"{mode.name}: got {final.parity_count}, " + f"expected {expected}" + ) + checked.append( + f"{mode.name}:{final.source_count}+" + f"{final.parity_count}" + ) + return "final blocks: " + ", ".join(checked) + + def zero_bad_restores_all_frames() -> str: + for mode in MODES: + schedule = schedules[mode.name].fec_schedule + flags = np.zeros(len(schedule.units), dtype=np.bool_) + repetition = ( + simulate_baseline( + schedule, flags, len(composites) + ) + if mode.parity_count == 0 + else simulate_fec( + schedule, flags, len(composites) + ) + ) + if ( + repetition.atomic_composite_frames_completed + != len(composites) + ): + raise AssertionError( + f"zero-Bad failed for {mode.name}" + ) + return "all eight modes restored 100% of frames without Bad" + + def fixed_seed_is_reproducible() -> str: + first = simulate_condition( + schedules["8+3"], + len(composites), + 0.05, + 329_329, + 3, + ) + second = simulate_condition( + schedules["8+3"], + len(composites), + 0.05, + 329_329, + 3, + ) + if first != second: + raise AssertionError("same seed changed result fields") + return "identical seed produced identical result fields" + + def queue_counts_all_parity() -> str: + baseline = schedules["none"].fec_schedule + for mode in protected_modes: + schedule = schedules[mode.name].fec_schedule + parity = sum( + int(unit.unit.is_parity) + for unit in schedule.units + ) + if len(schedule.units) != len( + baseline.source_packets + ) + parity: + raise AssertionError( + f"{mode.name}: parity absent from FIFO" + ) + if ( + schedule.total_transmitted_bytes + <= baseline.total_transmitted_bytes + ): + raise AssertionError( + f"{mode.name}: parity did not add bytes" + ) + return "every parity packet is included in FIFO timing" + + def overload_grows_queue() -> str: + source = schedules["8+4"].fec_schedule + overloaded = tuple( + TransmissionUnit( + sequence_index=unit.unit.sequence_index, + generation_time_seconds=( + unit.unit.generation_time_seconds * 0.5 + ), + wire_packet=unit.unit.wire_packet, + is_parity=unit.unit.is_parity, + block_id=unit.unit.block_id, + symbol_index=unit.unit.symbol_index, + source_global_index=( + unit.unit.source_global_index + ), + ) + for unit in source.units + ) + _, duration, mean_queue, max_queue = schedule_units( + overloaded + ) + offered_horizon = ( + source.source_duration_seconds * 0.5 + ) + offered_rate = ( + source.total_transmitted_bytes + * 8.0 + / offered_horizon + / 1000.0 + ) + if offered_rate <= CONTROL_STREAM_BITRATE_KBPS: + raise AssertionError("synthetic stream is not overloaded") + if duration <= offered_horizon or max_queue <= mean_queue: + raise AssertionError("overload did not grow the queue") + return ( + f"{offered_rate:.3f} kbit/s produced queue " + f"mean/max {mean_queue:.3f}/{max_queue}" + ) + + def incomplete_composite_is_not_published() -> str: + receiver = CompositeReassembler() + first_frame = [ + packet.inner_packet + for packet in source_packets + if packet.composite_frame_id == 0 + ] + for packet in first_frame[:-1]: + if receiver.ingest(packet) is not None: + raise AssertionError( + "incomplete composite was published" + ) + return "missing final ROI fragment prevented publication" + + def lab030_8_2_is_reproduced() -> str: + reference_units, reference_blocks = ( + build_lab030_mode_units( + source_packets, + FECMode("8+2", 2, "8+2"), + ) + ) + schedule = schedules["8+2"].fec_schedule + if tuple( + unit.wire_packet for unit in reference_units + ) != tuple( + unit.unit.wire_packet for unit in schedule.units + ): + raise AssertionError("8+2 wire order differs from Lab030") + if reference_blocks != schedule.blocks: + raise AssertionError("8+2 block plans differ from Lab030") + + with Path( + "data/processed/lab030/lab030_results.csv" + ).open(newline="", encoding="utf-8") as file: + reference_rows = { + float(row["mean_bad_duration_ms"]): row + for row in csv.DictReader(file) + if row["mode"] == "8+2" + } + current_rows = { + row.mean_bad_duration_ms: row + for row in results + if row.mode == "8+2" + } + for duration, current in current_rows.items(): + reference = reference_rows[duration] + if abs( + current.composite_success_rate + - float(reference["composite_success_rate"]) + ) > 1e-12: + raise AssertionError( + f"8+2 composite differs at {duration} ms" + ) + if abs( + current.p95_publication_delay_seconds + - float( + reference[ + "p95_publication_delay_seconds" + ] + ) + ) > 1e-12: + raise AssertionError( + f"8+2 delay differs at {duration} ms" + ) + return "8+2 exactly reproduced Lab030 order and four rows" + + tests.extend( + [ + ("all_k_r_supported", all_k_r_supported), + ("recover_any_r_erasures", recover_any_r_losses), + ("restored_inner_byte_exact", restored_inner_is_exact), + ("inner_outer_crc", crc_layers_pass), + ( + "final_partial_block_scaling", + final_partial_blocks_are_scaled, + ), + ("zero_bad_100_percent", zero_bad_restores_all_frames), + ("fixed_seed_reproducibility", fixed_seed_is_reproducible), + ("queue_counts_parity", queue_counts_all_parity), + ("overload_grows_queue", overload_grows_queue), + ( + "atomic_incomplete_composite", + incomplete_composite_is_not_published, + ), + ("lab030_8_2_reproduction", lab030_8_2_is_reproduced), + ] + ) + test_results = [] + for name, test in tests: + try: + detail = test() + except Exception as error: + test_results.append( + FunctionalTestResult(name, False, str(error)) + ) + else: + test_results.append( + FunctionalTestResult(name, True, detail) + ) + failed = [ + result for result in test_results if not result.passed + ] + if failed: + raise RuntimeError( + "Lab032 functional checks failed: " + + "; ".join( + f"{result.name}: {result.detail}" + for result in failed + ) + ) + return test_results + + +def validate_results(results: list[SimulationResult]) -> None: + expected = len(MODES) * len(MEAN_BAD_DURATIONS_SECONDS) + if len(results) != expected: + raise RuntimeError( + f"expected {expected} rows, got {len(results)}" + ) + keys = { + (result.mode, result.mean_bad_duration_ms) + for result in results + } + if len(keys) != expected: + raise RuntimeError("Lab032 result rows are not unique") + for result in results: + if not 0.0 <= result.composite_success_rate <= 1.0: + raise RuntimeError("composite success outside 0...1") + if not 0.0 <= result.base_success_rate <= 1.0: + raise RuntimeError("BASE success outside 0...1") + if not 0.0 <= result.roi_success_rate <= 1.0: + raise RuntimeError("ROI success outside 0...1") + + +def save_results_csv(results: list[SimulationResult]) -> None: + with CSV_PATH.open("w", newline="", encoding="utf-8") as file: + writer = csv.DictWriter(file, fieldnames=CSV_FIELDS) + writer.writeheader() + for result in results: + writer.writerow(asdict(result)) + + +def summary_rows( + results: list[SimulationResult], +) -> list[dict[str, object]]: + lookups = { + duration: result_lookup(results, duration) + for duration in (10.0, 50.0, 200.0, 1000.0) + } + rows = [] + for mode in MODES: + representative = lookups[200.0][mode.name] + row: dict[str, object] = { + "mode": mode.name, + "label": mode.label, + "source_block_size": mode.source_count, + "nominal_parity_count": mode.parity_count, + "code_rate": mode.code_rate, + "outer_fec_stream_bitrate_kbps": ( + representative.outer_fec_stream_bitrate_kbps + ), + "margin_to_control_bitrate_kbps": ( + representative.margin_to_control_bitrate_kbps + ), + "service_and_parity_percent": ( + representative.service_and_parity_percent + ), + "fec_block_count": representative.fec_block_count, + "mean_block_duration_seconds": ( + representative.mean_block_duration_seconds + ), + "max_block_duration_seconds": ( + representative.max_block_duration_seconds + ), + "mean_decode_ready_duration_seconds": ( + representative.mean_decode_ready_duration_seconds + ), + "mean_queue_length_packets": ( + representative.mean_queue_length_packets + ), + "max_queue_length_packets": ( + representative.max_queue_length_packets + ), + } + for duration in (10, 50, 200, 1000): + result = lookups[float(duration)][mode.name] + row[f"composite_success_{duration}ms"] = ( + result.composite_success_rate + ) + row[f"p95_publication_delay_{duration}ms_seconds"] = ( + result.p95_publication_delay_seconds + ) + row[f"p95_no_new_image_{duration}ms_seconds"] = ( + result.p95_no_new_image_duration_seconds + ) + rows.append(row) + return rows + + +def save_summary_csv(results: list[SimulationResult]) -> None: + rows = summary_rows(results) + with SUMMARY_CSV_PATH.open( + "w", newline="", encoding="utf-8" + ) as file: + writer = csv.DictWriter( + file, fieldnames=list(rows[0]) + ) + writer.writeheader() + writer.writerows(rows) + + +def _rows_for_mode( + results: list[SimulationResult], + mode_name: str, +) -> list[SimulationResult]: + return sorted( + ( + result + for result in results + if result.mode == mode_name + ), + key=lambda result: result.mean_bad_duration_ms, + ) + + +def _save_line_plot( + results: list[SimulationResult], + field: str, + ylabel: str, + title: str, + path: Path, +) -> None: + figure, axis = plt.subplots(figsize=(11.0, 6.5)) + for mode in MODES: + rows = _rows_for_mode(results, mode.name) + axis.plot( + [row.mean_bad_duration_ms for row in rows], + [getattr(row, field) for row in rows], + marker="o", + linewidth=1.8, + label=mode.label, + ) + axis.set_xscale("log") + axis.set_xlabel("Средняя длительность Bad, мс") + axis.set_ylabel(ylabel) + axis.set_title(title) + axis.grid(True, which="both", alpha=0.28) + axis.legend(ncol=2, fontsize=8) + figure.tight_layout() + figure.savefig(path, dpi=160) + plt.close(figure) + + +def practical_frontier( + results: list[SimulationResult], + mean_bad_duration_ms: float = 200.0, +) -> list[SimulationResult]: + rows = list( + result_lookup(results, mean_bad_duration_ms).values() + ) + frontier = [] + for candidate in rows: + dominated = any( + other.mode != candidate.mode + and ( + other.outer_fec_stream_bitrate_kbps + <= candidate.outer_fec_stream_bitrate_kbps + ) + and ( + other.p95_publication_delay_seconds + <= candidate.p95_publication_delay_seconds + ) + and ( + other.composite_success_rate + >= candidate.composite_success_rate + ) + and ( + other.outer_fec_stream_bitrate_kbps + < candidate.outer_fec_stream_bitrate_kbps + or other.p95_publication_delay_seconds + < candidate.p95_publication_delay_seconds + or other.composite_success_rate + > candidate.composite_success_rate + ) + for other in rows + ) + if not dominated: + frontier.append(candidate) + return sorted( + frontier, + key=lambda result: result.outer_fec_stream_bitrate_kbps, + ) + + +def save_plots(results: list[SimulationResult]) -> None: + _save_line_plot( + results, + "composite_success_rate", + "Доля восстановленных составных кадров", + "Lab032. Атомарное восстановление BASE + ROI", + COMPOSITE_SUCCESS_PLOT_PATH, + ) + _save_line_plot( + results, + "p95_publication_delay_seconds", + "P95 задержки публикации, с", + "Lab032. Задержка публикации составного кадра", + PUBLICATION_DELAY_PLOT_PATH, + ) + _save_line_plot( + results, + "p95_no_new_image_duration_seconds", + "P95 отсутствия нового изображения, с", + "Lab032. Длительность удержания изображения", + NO_IMAGE_PLOT_PATH, + ) + + representative = result_lookup(results, 200.0) + rows = [representative[mode.name] for mode in MODES] + positions = np.arange(len(rows)) + figure, rate_axis = plt.subplots(figsize=(11.4, 6.5)) + overhead_axis = rate_axis.twinx() + rate_bars = rate_axis.bar( + positions - 0.2, + [row.outer_fec_stream_bitrate_kbps for row in rows], + width=0.4, + color="#1565c0", + label="Внешний поток", + ) + overhead_bars = overhead_axis.bar( + positions + 0.2, + [row.service_and_parity_percent for row in rows], + width=0.4, + color="#ef6c00", + alpha=0.72, + label="Служебная доля", + ) + limit_line = rate_axis.axhline( + CONTROL_STREAM_BITRATE_KBPS, + color="#b71c1c", + linestyle="--", + linewidth=1.5, + label="300 кбит/с", + ) + rate_axis.set_xticks( + positions, + [mode.label for mode in MODES], + rotation=24, + ha="right", + ) + rate_axis.set_ylabel("Внешний поток, кбит/с") + overhead_axis.set_ylabel("Служебные и parity, %") + rate_axis.set_title("Lab032. Скорость потока и избыточность") + rate_axis.grid(True, axis="y", alpha=0.25) + rate_axis.legend( + [rate_bars, overhead_bars, limit_line], + ["Внешний поток", "Служебная доля", "300 кбит/с"], + loc="upper left", + ) + figure.tight_layout() + figure.savefig(STREAM_OVERHEAD_PLOT_PATH, dpi=160) + plt.close(figure) + + equal_rate_modes = ("4+1", "8+2", "12+3") + figure, success_axis = plt.subplots(figsize=(10.4, 6.3)) + decode_axis = success_axis.twinx() + block_sizes = [ + representative[name].source_block_size + for name in equal_rate_modes + ] + for duration in (10.0, 50.0, 200.0, 1000.0): + lookup = result_lookup(results, duration) + success_axis.plot( + block_sizes, + [ + lookup[name].composite_success_rate + for name in equal_rate_modes + ], + marker="o", + linewidth=1.8, + label=f"Bad {duration:.0f} мс", + ) + decode_axis.plot( + block_sizes, + [ + representative[ + name + ].mean_decode_ready_duration_seconds + for name in equal_rate_modes + ], + color="#c62828", + marker="s", + linestyle="--", + linewidth=2.0, + label="До декодирования", + ) + success_axis.set_xticks(block_sizes) + success_axis.set_xlabel("Число исходных пакетов k") + success_axis.set_ylabel("Доля восстановленных кадров") + decode_axis.set_ylabel("Среднее время до декодирования, с") + success_axis.set_title( + "Lab032. Влияние размера блока при кодовой скорости 0.8" + ) + success_axis.grid(True, alpha=0.28) + lines = success_axis.lines + decode_axis.lines + success_axis.legend( + lines, + [line.get_label() for line in lines], + loc="best", + fontsize=8, + ) + figure.tight_layout() + figure.savefig(BLOCK_SIZE_PLOT_PATH, dpi=160) + plt.close(figure) + + figure, axis = plt.subplots(figsize=(10.7, 6.5)) + delay_values = np.array( + [row.p95_publication_delay_seconds for row in rows] + ) + scatter = axis.scatter( + [row.outer_fec_stream_bitrate_kbps for row in rows], + [row.composite_success_rate for row in rows], + c=delay_values, + s=100, + cmap="viridis_r", + edgecolors="black", + linewidths=0.6, + ) + for row in rows: + axis.annotate( + row.label, + ( + row.outer_fec_stream_bitrate_kbps, + row.composite_success_rate, + ), + xytext=(5, 5), + textcoords="offset points", + fontsize=8, + ) + frontier = practical_frontier(results) + axis.plot( + [ + row.outer_fec_stream_bitrate_kbps + for row in frontier + ], + [row.composite_success_rate for row in frontier], + color="#d32f2f", + linestyle="--", + linewidth=1.4, + label="Недоминируемая граница", + ) + axis.axvline( + CONTROL_STREAM_BITRATE_KBPS, + color="#555555", + linestyle=":", + linewidth=1.3, + ) + axis.set_xlabel("Внешний поток, кбит/с") + axis.set_ylabel("Доля восстановленных кадров") + axis.set_title( + "Lab032. Практическая граница при Bad 200 мс" + ) + axis.grid(True, alpha=0.25) + axis.legend(loc="lower right") + colorbar = figure.colorbar(scatter, ax=axis) + colorbar.set_label("P95 задержки публикации, с") + figure.tight_layout() + figure.savefig(PRACTICAL_FRONTIER_PLOT_PATH, dpi=160) + plt.close(figure) + + +def _summary_table( + results: list[SimulationResult], +) -> list[str]: + lookups = { + duration: result_lookup(results, duration) + for duration in (10.0, 50.0, 200.0, 1000.0) + } + lines = [ + ( + "mode | rate | outer/margin kbit/s | composite " + "10/50/200/1000 ms | pub p95 200 ms s | no-image " + "p95 200 ms s | block mean/max ms | decode mean ms | " + "queue mean/max" + ), + ( + "----:|-----:|----------------------:|" + "----------------------------:|-------------------:" + "|------------------------:|------------------:" + "|---------------:|---------------:" + ), + ] + for mode in MODES: + row = lookups[200.0][mode.name] + composites = "/".join( + f"{lookups[duration][mode.name].composite_success_rate:.6f}" + for duration in (10.0, 50.0, 200.0, 1000.0) + ) + lines.append( + f"{mode.label} | {mode.code_rate:.6f} | " + f"{row.outer_fec_stream_bitrate_kbps:.3f}/" + f"{row.margin_to_control_bitrate_kbps:.3f} | " + f"{composites} | " + f"{row.p95_publication_delay_seconds:.6f} | " + f"{row.p95_no_new_image_duration_seconds:.6f} | " + f"{row.mean_block_duration_seconds * 1000:.3f}/" + f"{row.max_block_duration_seconds * 1000:.3f} | " + f"{row.mean_decode_ready_duration_seconds * 1000:.3f} | " + f"{row.mean_queue_length_packets:.3f}/" + f"{row.max_queue_length_packets}" + ) + return lines + + +def _detailed_200ms_table( + results: list[SimulationResult], +) -> list[str]: + rows = result_lookup(results, 200.0) + lines = [ + ( + "mode | BASE/ROI/composite | blocks ok/fail | restored " + "packets | delivered kbit/s | publication mean/p95/max s " + "| no-image mean/p95/max s | incomplete mean/p95/max" + ), + ( + "----:|-------------------:|---------------:|" + "-----------------:|-----------------:|" + "--------------------------:|-----------------------:" + "|------------------------:" + ), + ] + for mode in MODES: + row = rows[mode.name] + lines.append( + f"{mode.label} | {row.base_success_rate:.6f}/" + f"{row.roi_success_rate:.6f}/" + f"{row.composite_success_rate:.6f} | " + f"{row.fec_all_block_success_rate:.6f}/" + f"{row.fec_unrecoverable_block_rate:.6f} | " + f"{row.recovered_source_packets} | " + f"{row.effective_delivered_video_bitrate_kbps:.3f} | " + f"{row.mean_publication_delay_seconds:.6f}/" + f"{row.p95_publication_delay_seconds:.6f}/" + f"{row.max_publication_delay_seconds:.6f} | " + f"{row.mean_no_new_image_duration_seconds:.6f}/" + f"{row.p95_no_new_image_duration_seconds:.6f}/" + f"{row.max_no_new_image_duration_seconds:.6f} | " + f"{row.mean_consecutive_incomplete_frames:.3f}/" + f"{row.p95_consecutive_incomplete_frames:.3f}/" + f"{row.max_consecutive_incomplete_frames}" + ) + return lines + + +def write_report( + metadata: VideoMetadata, + composites: list[EncodedComposite], + schedules: dict[str, ParameterSchedule], + results: list[SimulationResult], + tests: list[FunctionalTestResult], +) -> None: + representative = result_lookup(results, 200.0) + equal_rate = [ + representative[name] + for name in ("4+1", "8+2", "12+3") + ] + strong = [ + representative[name] for name in ("4+2", "8+4") + ] + intermediate = representative["8+3"] + lower = representative["8+2"] + upper = representative["8+4"] + frontier = practical_frontier(results) + best_resilience = max( + representative.values(), + key=lambda result: result.composite_success_rate, + ) + minimum_rate = min( + representative.values(), + key=lambda result: result.outer_fec_stream_bitrate_kbps, + ) + protected = [ + row + for row in representative.values() + if row.nominal_parity_count + ] + minimum_protected_delay = min( + protected, + key=lambda result: result.p95_publication_delay_seconds, + ) + + lines = [ + "Lab032. Подбор параметров блочного исправления потерь", + "", + "Исходный профиль и неизменные слои", + f"- Видео: {SOURCE_VIDEO_PATH}", + ( + f"- {len(composites)} реальных пар BASE/ROI, " + f"{COMPOSITE_FPS:.0f} fps; длительность " + f"{metadata.duration_seconds:.6f} с." + ), + ( + "- BASE 240x135 grayscale JPEG Q23; ROI 320x180 " + "grayscale JPEG Q33." + ), + ( + "- Внутренний Lab028 packet: payload 512 байт; внешний " + "формат и GF(256) Lab030 не изменены." + ), + ( + "- Глубина D=1: systematic symbols блока передаются " + "первыми, затем parity; перемежение отсутствует." + ), + ( + "- Блоки формируются из непрерывного потока внутренних " + "пакетов и могут пересекать composite_frame_id." + ), + "", + "Последний неполный блок", + "- Используется единое правило:", + " r_last = max(1, ceil(k_last × r / k)).", + ( + "- Нулевое дополнение существует только внутри математики " + "GF(256); внешние пакеты синтетически не добавляются." + ), + "", + "Сравнительная таблица восьми режимов", + *_summary_table(results), + "", + "Подробные результаты при средней Bad 200 мс", + *_detailed_200ms_table(results), + "", + "Временная модель и очередь", + ( + "- Непрерывные чередующиеся экспоненциальные Good/Bad " + "интервалы Lab029B, средняя доля Bad около 2%." + ), + ( + "- Средние Bad 10, 50, 200 и 1000 мс; 200 повторов; " + f"fixed seeds {SEED_BASE}...{SEED_BASE + 3}." + ), + ( + "- Пакет теряется при любом пересечении передачи с Bad. " + "FIFO учитывает фактическую длину каждого внешнего пакета " + "и скорость 300 кбит/с." + ), + ( + "- Все исследованные режимы остаются ниже 300 кбит/с; " + "положительный запас показан в таблице. Отдельная " + "функциональная проверка ускоряет поток 8+4 вдвое и " + "подтверждает реальное накопление очереди при перегрузке." + ), + ( + "- Block duration измеряется от начала первого до конца " + "последнего символа. Decode-ready без потерь — от начала " + "первого до конца k-го systematic symbol." + ), + "", + "1. Одинаковая кодовая скорость 0.8: 4+1, 8+2 и 12+3", + *[ + ( + f"- {row.label}: outer " + f"{row.outer_fec_stream_bitrate_kbps:.3f} кбит/с, " + f"block mean " + f"{row.mean_block_duration_seconds * 1000:.3f} мс, " + f"decode-ready " + f"{row.mean_decode_ready_duration_seconds * 1000:.3f} " + f"мс, composite@200 " + f"{row.composite_success_rate:.6f}." + ) + for row in equal_rate + ], + ( + "- При одинаковой номинальной code rate длина блока " + "меняет временное окно, в котором одна помеха может собрать " + "несколько стираний. Короткий блок раньше получает k " + "systematic symbols и быстрее допускает декодирование." + ), + ( + "- Длинный блок дольше остаётся открытым и может включить " + "больше потерянных пакетов одной длительной помехи; при этом " + "его больший абсолютный r иногда полезен для рассеянных " + "стираний." + ), + "", + "2. Одинаковая code rate 2/3: 4+2 и 8+4", + *[ + ( + f"- {row.label}: outer " + f"{row.outer_fec_stream_bitrate_kbps:.3f} кбит/с, " + f"composite@200 " + f"{row.composite_success_rate:.6f}, " + f"publication P95 " + f"{row.p95_publication_delay_seconds:.6f} с, " + f"no-image P95 " + f"{row.p95_no_new_image_duration_seconds:.6f} с." + ) + for row in strong + ], + ( + "- Сравнение изолирует влияние длины блока при одинаковой " + "сильной избыточности: короткий 4+2 быстрее закрывается, " + "длинный 8+4 имеет больше абсолютных parity symbols, но " + "дольше накапливает стирания." + ), + "", + "3. Влияние увеличения r", + ( + "- Большее r позволяет исправить больше стираний, но " + "увеличивает внешний поток, служебную долю, время передачи " + "блока и публикационную задержку." + ), + ( + f"- 8+3: outer " + f"{intermediate.outer_fec_stream_bitrate_kbps:.3f} кбит/с " + f"(8+2 {lower.outer_fec_stream_bitrate_kbps:.3f}, " + f"8+4 {upper.outer_fec_stream_bitrate_kbps:.3f}); " + f"composite@200 {intermediate.composite_success_rate:.6f} " + f"(8+2 {lower.composite_success_rate:.6f}, " + f"8+4 {upper.composite_success_rate:.6f}); " + f"P95 delay {intermediate.p95_publication_delay_seconds:.6f} " + f"с (8+2 {lower.p95_publication_delay_seconds:.6f}, " + f"8+4 {upper.p95_publication_delay_seconds:.6f})." + ), + ( + "- Поэтому 8+3 оценивается по фактическому положению между " + "8+2 и 8+4 сразу по скорости, устойчивости и задержке, без " + "автоматического назначения рабочим режимом." + ), + "", + "4. Практическая граница без автоматического выбора", + ( + f"- Минимальный поток: {minimum_rate.label}, " + f"{minimum_rate.outer_fec_stream_bitrate_kbps:.3f} кбит/с." + ), + ( + f"- Максимальная composite-устойчивость при Bad 200 мс: " + f"{best_resilience.label}, " + f"{best_resilience.composite_success_rate:.6f}." + ), + ( + f"- Минимальная P95-задержка среди защищённых режимов: " + f"{minimum_protected_delay.label}, " + f"{minimum_protected_delay.p95_publication_delay_seconds:.6f} " + "с." + ), + ( + "- Недоминируемые по outer bitrate↓, publication P95↓ и " + "composite success↑ режимы при Bad 200 мс: " + + ", ".join(row.label for row in frontier) + + "." + ), + ( + "- Это набор компромиссов, а не автоматический выбор " + "единственного режима." + ), + "", + "Функциональные проверки", + *[ + f"- {'PASS' if test.passed else 'FAIL'} " + f"{test.name}: {test.detail}" + for test in tests + ], + "", + "Допущения модели", + ( + "- JPEG кодируются один раз в памяти; вычислительная " + "задержка GF(256) считается нулевой." + ), + ( + "- Стирается весь внешний пакет; отдельные битовые ошибки " + "не моделируются, но оба уровня CRC проверяются." + ), + ( + "- Publication delay считается от frame_id/3 до конца " + "пакета, завершившего атомарную сборку BASE+ROI." + ), + ( + "- No-image заканчивается публикацией следующего полного " + "кадра или концом расписания; неполный кадр не публикуется." + ), + ( + "- ARQ, повторы, глубокое перемежение, команды, телеметрия, " + "модуляция и реальный SDR отсутствуют." + ), + "", + "Артефакты", + f"- Полный CSV: {CSV_PATH}", + f"- Сводный CSV: {SUMMARY_CSV_PATH}", + f"- Отчёт: {REPORT_PATH}", + f"- Доля восстановленных кадров: {COMPOSITE_SUCCESS_PLOT_PATH}", + f"- Скорость и избыточность: {STREAM_OVERHEAD_PLOT_PATH}", + f"- Задержка публикации: {PUBLICATION_DELAY_PLOT_PATH}", + f"- Отсутствие изображения: {NO_IMAGE_PLOT_PATH}", + f"- Влияние размера блока: {BLOCK_SIZE_PLOT_PATH}", + f"- Практическая граница: {PRACTICAL_FRONTIER_PLOT_PATH}", + "- JPEG, пакеты и бинарные дампы не сохранялись.", + "", + ] + REPORT_PATH.write_text("\n".join(lines), encoding="utf-8") + + +def validate_outputs() -> None: + expected = ( + CSV_PATH, + SUMMARY_CSV_PATH, + REPORT_PATH, + COMPOSITE_SUCCESS_PLOT_PATH, + STREAM_OVERHEAD_PLOT_PATH, + PUBLICATION_DELAY_PLOT_PATH, + NO_IMAGE_PLOT_PATH, + BLOCK_SIZE_PLOT_PATH, + PRACTICAL_FRONTIER_PLOT_PATH, + ) + for path in expected: + if not path.exists() or path.stat().st_size <= 0: + raise RuntimeError(f"missing or empty output: {path}") + with CSV_PATH.open(newline="", encoding="utf-8") as file: + rows = list(csv.DictReader(file)) + if len(rows) != 32 or len(rows[0]) != len(CSV_FIELDS): + raise RuntimeError("Lab032 full CSV shape is invalid") + with SUMMARY_CSV_PATH.open( + newline="", encoding="utf-8" + ) as file: + summary = list(csv.DictReader(file)) + if len(summary) != 8: + raise RuntimeError("Lab032 summary CSV must have eight rows") + for path in expected: + if path.suffix == ".png": + image = cv2.imread(str(path), cv2.IMREAD_UNCHANGED) + if image is None: + raise RuntimeError(f"OpenCV cannot read {path}") + + +def main() -> None: + print("Lab032: loading real 512-byte-payload Lab028 stream...") + metadata, composites = load_video_profile(SOURCE_VIDEO_PATH) + profile = prepare_profiles(composites)[INNER_PAYLOAD_SIZE] + schedules = build_schedules(metadata, composites, profile) + for mode in MODES: + schedule = schedules[mode.name] + fec = schedule.fec_schedule + rate = ( + fec.total_transmitted_bytes + * 8.0 + / metadata.duration_seconds + / 1000.0 + ) + print( + f" {mode.label}: blocks={len(fec.blocks)}, " + f"units={len(fec.units)}, offered={rate:.3f} kbit/s, " + f"block={schedule.mean_block_duration_seconds:.6f} s, " + f"queue={fec.mean_queue_length_packets:.3f}/" + f"{fec.max_queue_length_packets}" + ) + + print( + f"Running {len(MODES) * len(MEAN_BAD_DURATIONS_SECONDS)} " + f"conditions, {MONTE_CARLO_REPETITIONS} repetitions each..." + ) + results = run_monte_carlo(schedules, len(composites)) + validate_results(results) + + print("Running Lab032 functional checks...") + tests = run_functional_tests( + composites, schedules, results + ) + for test in tests: + print(f" PASS {test.name}: {test.detail}") + + OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True) + save_results_csv(results) + save_summary_csv(results) + save_plots(results) + write_report( + metadata, composites, schedules, results, tests + ) + validate_outputs() + + representative = result_lookup(results, 200.0) + print("Representative Bad=200 ms results:") + for mode in MODES: + result = representative[mode.name] + print( + f" {mode.label}: outer=" + f"{result.outer_fec_stream_bitrate_kbps:.3f} kbit/s, " + f"composite={result.composite_success_rate:.6f}, " + f"delay_p95={result.p95_publication_delay_seconds:.6f} s" + ) + print(f"Full CSV: {CSV_PATH}") + print(f"Summary CSV: {SUMMARY_CSV_PATH}") + print(f"Report: {REPORT_PATH}") + print("Lab032 completed successfully.") + + +if __name__ == "__main__": + main()