diff --git a/PROJECT_LOG.md b/PROJECT_LOG.md index 6c20762..5dcf122 100644 --- a/PROJECT_LOG.md +++ b/PROJECT_LOG.md @@ -264,3 +264,25 @@ git version 2.51.1.windows.1 - После восстановления скорости возраст изображения снизился ниже 500 мс за 0,077 секунды. - Политика двух ожидающих кадров оставлена резервной. - Следующая лабораторная объединяет планировщик с моделью ошибок радиоканала и защитой команд управления. + +--- + +# Запись 012 + +## Дата + +4 августа 2026 года + +## Тема + +Завершение Lab037: полный транспорт с серийными потерями и повторением критических команд. + +## Выполнено + +- Lab037 объединяет видеопоток, обычные и аварийные команды, телеметрию и временные серийные потери пакетов. +- Близкие повторные копии команд часто попадают в один и тот же интервал помехи. +- Повторение Lab037 лишь умеренно уменьшило долю недоставленных управляющих состояний. +- Максимальный промежуток без свежей команды остался более одной секунды. +- Своевременная доставка аварийной команды по радиоканалу не гарантируется. +- Для безопасного поведения при потере связи требуется локальный сторожевой таймер на ровере. +- Следующий этап исследует сторожевой таймер, временно разнесённую аварийную команду и подтверждение получения. diff --git a/data/processed/lab037/lab037_command_age.png b/data/processed/lab037/lab037_command_age.png new file mode 100644 index 0000000..6d0c982 Binary files /dev/null and b/data/processed/lab037/lab037_command_age.png differ diff --git a/data/processed/lab037/lab037_command_gap.png b/data/processed/lab037/lab037_command_gap.png new file mode 100644 index 0000000..23fba2f Binary files /dev/null and b/data/processed/lab037/lab037_command_gap.png differ diff --git a/data/processed/lab037/lab037_command_metrics.csv b/data/processed/lab037/lab037_command_metrics.csv new file mode 100644 index 0000000..752e837 --- /dev/null +++ b/data/processed/lab037/lab037_command_metrics.csv @@ -0,0 +1,37 @@ +channel_kbps,mean_bad_duration_ms,protection_mode,original_commands,repeat_copies_created,delivered_unique_states_mean,states_with_all_copies_lost_mean,undelivered_state_fraction,mean_age_ms,p95_age_ms,max_age_ms,max_gap_without_fresh_command_ms,gaps_over_100ms_mean,maximum_consecutive_undelivered_states,cancelled_pending_repeats,suppressed_duplicates_mean,repetition_overhead_kbps +300.0,10.0,none,2400,0,2342.91,57.09,0.0237875,10.064207370338313,17.46633333333314,17.787000000083708,212.45300000001066,30.9,3,0,0.0,0.0 +300.0,10.0,emergency_only,2400,0,2342.96,57.04,0.023766666666666665,10.053455724115054,17.46633333333314,17.787000000083708,212.45300000001066,30.9,3,0,0.0,0.0 +300.0,10.0,emergency_and_control,2400,2400,2392.61,7.39,0.0030791666666666667,8.45785116812213,17.492999999987546,40.39966666671546,156.93333333337023,4.31,2,0,2294.1,10.24 +300.0,50.0,none,2400,0,2350.09,49.91,0.020795833333333333,10.06390907158428,17.46633333333314,17.787000000083708,463.83999999999935,23.36,8,0,0.0,0.0 +300.0,50.0,emergency_only,2400,0,2350.12,49.88,0.020783333333333334,10.053007932945025,17.46633333333314,17.787000000083708,463.83999999999935,23.34,8,0,0.0,0.0 +300.0,50.0,emergency_and_control,2400,2400,2368.81,31.19,0.012995833333333333,8.149776716309221,17.466333333345574,40.39966666671546,437.49366666666134,16.52,7,0,2331.59,10.24 +300.0,200.0,none,2400,0,2352.94,47.06,0.019608333333333332,10.065125598912898,17.46633333333314,17.787000000083708,1314.5596666666534,9.69,25,0,0.0,0.0 +300.0,200.0,emergency_only,2400,0,2352.93,47.07,0.0196125,10.054171590331793,17.46633333333314,17.787000000083708,1314.5596666666534,9.68,25,0,0.0,0.0 +300.0,200.0,emergency_and_control,2400,2400,2358.25,41.75,0.017395833333333333,8.011422829086634,17.466333333345574,40.39966666671546,1263.7329999999736,8.96,24,0,2347.22,10.24 +300.0,1000.0,none,2400,0,2356.0,44.0,0.018333333333333333,10.063361721861758,17.46633333333314,17.787000000083708,8264.347000000002,2.08,164,0,0.0,0.0 +300.0,1000.0,emergency_only,2400,0,2356.0,44.0,0.018333333333333333,10.052216956723077,17.46633333333314,17.787000000083708,8264.347000000002,2.08,164,0,0.0,0.0 +300.0,1000.0,emergency_and_control,2400,2400,2357.05,42.95,0.017895833333333333,7.961249001941846,17.466333333345574,40.39966666671546,8256.667000000003,2.04,164,0,2355.06,10.24 +260.0,10.0,none,2400,0,2342.33,57.67,0.024029166666666667,10.587096251572822,19.00546153706273,20.66700000090549,212.89230769232148,38.14,3,0,0.0,0.0 +260.0,10.0,emergency_only,2400,0,2342.54,57.46,0.023941666666666667,10.55155836426941,18.943923074033364,20.66700000090549,212.89230769232148,37.99,3,0,0.0,0.0 +260.0,10.0,emergency_and_control,2400,2400,2391.93,8.07,0.0033625,11.524501787594767,20.071461539679092,43.08684615433833,208.5538461538441,4.86,3,0,2294.69,10.24 +260.0,50.0,none,2400,0,2351.04,48.96,0.0204,10.589026132078255,19.00546153706273,20.66700000090549,593.5692307692477,24.94,11,0,0.0,0.0 +260.0,50.0,emergency_only,2400,0,2351.03,48.97,0.020404166666666668,10.554321727015681,18.943923074033364,20.66700000090549,593.5692307692477,24.95,11,0,0.0,0.0 +260.0,50.0,emergency_and_control,2400,2400,2368.11,31.89,0.0132875,11.22534996686765,19.856076923090882,43.086846153229885,607.0461538461665,16.8,11,0,2331.95,10.24 +260.0,200.0,none,2400,0,2352.39,47.61,0.0198375,10.590370607315748,19.00546153706273,20.66700000090549,1208.7692307691782,10.14,23,0,0.0,0.0 +260.0,200.0,emergency_only,2400,0,2352.37,47.63,0.019845833333333333,10.555450779041617,18.943923074033364,20.66700000090549,1208.7692307691782,10.14,23,0,0.0,0.0 +260.0,200.0,emergency_and_control,2400,2400,2357.54,42.46,0.017691666666666668,11.102944502795141,19.794538462733158,43.086846151908276,1183.5076923075007,9.08,22,0,2346.99,10.24 +260.0,1000.0,none,2400,0,2349.53,50.47,0.021029166666666668,10.59190909829356,19.00546153706273,20.66700000090549,5591.600000002416,2.45,111,0,0.0,0.0 +260.0,1000.0,emergency_only,2400,0,2349.51,50.49,0.0210375,10.554948885192662,18.943923074033364,20.66700000090549,5591.600000002416,2.45,111,0,0.0,0.0 +260.0,1000.0,emergency_and_control,2400,2400,2350.67,49.33,0.020554166666666665,11.063445548931949,19.763769232241657,42.93300000071554,5596.553846152844,2.43,111,0,2348.37,10.24 +230.0,10.0,none,2400,0,2341.62,58.38,0.024325,12.548624274642515,22.260869572619413,23.26956522753676,220.06956521742893,30.19,3,0,0.0,0.0 +230.0,10.0,emergency_only,2400,0,2342.24,57.76,0.024066666666666667,12.466533563436789,22.10434782596593,26.608695653614234,205.9130434783043,29.82,3,0,0.0,0.0 +230.0,10.0,emergency_and_control,2400,2400,2389.52,10.48,0.004366666666666666,13.105058396153792,22.747826096122026,45.86086956628321,180.52173913048364,5.72,2,0,2292.76,10.24 +230.0,50.0,none,2400,0,2351.4,48.6,0.02025,12.546836559947174,22.278260869718736,23.26956522753676,504.00000000009015,22.11,9,0,0.0,0.0 +230.0,50.0,emergency_only,2400,0,2350.83,49.17,0.0204875,12.46013047739179,22.086956522841206,26.608695653614234,513.2521739130347,22.12,9,0,0.0,0.0 +230.0,50.0,emergency_and_control,2400,2400,2368.33,31.67,0.013195833333333334,12.812162453675606,22.48695652279764,45.86086956510371,509.7043478262151,15.51,9,0,2333.53,10.24 +230.0,200.0,none,2400,0,2351.31,48.69,0.0202875,12.546997135592148,22.260869572619413,23.26956522753676,1650.226086956607,9.84,32,0,0.0,0.0 +230.0,200.0,emergency_only,2400,0,2351.29,48.71,0.020295833333333332,12.460327072890104,22.086956522841206,26.608695653614234,1651.200000000088,9.76,32,0,0.0,0.0 +230.0,200.0,emergency_and_control,2400,2400,2356.73,43.27,0.018029166666666666,12.69135986301433,22.40000000105624,45.86086956523161,1655.9999999999775,8.94,32,0,2346.14,10.24 +230.0,1000.0,none,2400,0,2350.23,49.77,0.0207375,12.54769093279944,22.260869572619413,23.26956522753676,4894.226086956465,2.34,97,0,0.0,0.0 +230.0,1000.0,emergency_only,2400,0,2350.23,49.77,0.0207375,12.464516020436964,22.10434782596593,26.608695653614234,4894.226086956465,2.29,97,0,0.0,0.0 +230.0,1000.0,emergency_and_control,2400,2400,2351.43,48.57,0.0202375,12.650200538271994,22.382608697000705,45.75652174048628,4875.8956521739165,2.28,96,0,2349.19,10.24 diff --git a/data/processed/lab037/lab037_emergency_metrics.csv b/data/processed/lab037/lab037_emergency_metrics.csv new file mode 100644 index 0000000..2e03272 --- /dev/null +++ b/data/processed/lab037/lab037_emergency_metrics.csv @@ -0,0 +1,109 @@ +channel_kbps,mean_bad_duration_ms,protection_mode,event_time_seconds,delivered_fraction,mean_first_delivery_ms,p95_first_delivery_ms,deadline_50ms_fraction,lost_copies_mean,duplicate_copies_mean +300.0,10.0,none,30.0,0.96,1.7066666666671892,1.7066666666671892,0.96,0.04,0.0 +300.0,10.0,none,60.0,0.98,1.7066666666636365,1.7066666666636365,0.98,0.02,0.0 +300.0,10.0,none,90.0,0.98,1.7066666666636365,1.7066666666636365,0.98,0.02,0.0 +300.0,10.0,emergency_only,30.0,1.0,2.5941333333338434,1.7066666666671892,1.0,0.12,1.88 +300.0,10.0,emergency_only,60.0,0.99,1.9307744107713416,1.7066666666636365,0.99,0.06,1.95 +300.0,10.0,emergency_only,90.0,1.0,2.1503999999970347,1.7066666666636365,1.0,0.06,1.94 +300.0,10.0,emergency_and_control,30.0,1.0,2.5941333333338434,1.7066666666671892,1.0,0.12,1.88 +300.0,10.0,emergency_and_control,60.0,1.0,2.346666666663495,1.7066666666636365,1.0,0.05,1.95 +300.0,10.0,emergency_and_control,90.0,1.0,2.1503999999970347,1.7066666666636365,1.0,0.06,1.94 +300.0,50.0,none,30.0,0.98,1.7066666666671892,1.7066666666671892,0.98,0.02,0.0 +300.0,50.0,none,60.0,0.97,1.7066666666636365,1.7066666666636365,0.97,0.03,0.0 +300.0,50.0,none,90.0,0.98,1.7066666666636365,1.7066666666636365,0.98,0.02,0.0 +300.0,50.0,emergency_only,30.0,0.99,1.9307744107749303,1.7066666666671892,0.99,0.05,1.96 +300.0,50.0,emergency_only,60.0,0.98,1.9330612244867265,1.7066666666636365,0.98,0.13,1.89 +300.0,50.0,emergency_only,90.0,0.99,1.9307744107714133,1.7066666666636365,0.99,0.06,1.95 +300.0,50.0,emergency_and_control,30.0,0.99,1.9307744107749303,1.7066666666671892,0.99,0.05,1.96 +300.0,50.0,emergency_and_control,60.0,0.98,1.9330612244867265,1.7066666666636365,0.98,0.13,1.89 +300.0,50.0,emergency_and_control,90.0,0.99,1.9307744107714133,1.7066666666636365,0.99,0.06,1.95 +300.0,200.0,none,30.0,0.97,1.7066666666671892,1.7066666666671892,0.97,0.03,0.0 +300.0,200.0,none,60.0,0.99,1.7066666666636365,1.7066666666636365,0.99,0.01,0.0 +300.0,200.0,none,90.0,0.98,1.7066666666636365,1.7066666666636365,0.98,0.02,0.0 +300.0,200.0,emergency_only,30.0,0.97,1.7066666666671892,1.7066666666671892,0.97,0.12,1.91 +300.0,200.0,emergency_only,60.0,0.99,1.7066666666636365,1.7066666666636365,0.99,0.03,1.98 +300.0,200.0,emergency_only,90.0,0.98,1.7066666666636365,1.7066666666636365,0.98,0.08,1.94 +300.0,200.0,emergency_and_control,30.0,0.97,1.7066666666671892,1.7066666666671892,0.97,0.12,1.91 +300.0,200.0,emergency_and_control,60.0,0.99,1.7066666666636365,1.7066666666636365,0.99,0.03,1.98 +300.0,200.0,emergency_and_control,90.0,0.98,1.7066666666636365,1.7066666666636365,0.98,0.08,1.94 +300.0,1000.0,none,30.0,0.96,1.7066666666671892,1.7066666666671892,0.96,0.04,0.0 +300.0,1000.0,none,60.0,0.97,1.7066666666636365,1.7066666666636365,0.97,0.03,0.0 +300.0,1000.0,none,90.0,1.0,1.7066666666636365,1.7066666666636365,1.0,0.0,0.0 +300.0,1000.0,emergency_only,30.0,0.96,1.7066666666671892,1.7066666666671892,0.96,0.12,1.92 +300.0,1000.0,emergency_only,60.0,0.98,2.1159183673438346,1.7066666666636365,0.98,0.08,1.94 +300.0,1000.0,emergency_only,90.0,1.0,1.7066666666636365,1.7066666666636365,1.0,0.0,2.0 +300.0,1000.0,emergency_and_control,30.0,0.96,1.7066666666671892,1.7066666666671892,0.96,0.12,1.92 +300.0,1000.0,emergency_and_control,60.0,0.98,2.133333333330198,1.7066666666636365,0.98,0.08,1.94 +300.0,1000.0,emergency_and_control,90.0,1.0,1.7066666666636365,1.7066666666636365,1.0,0.0,2.0 +260.0,10.0,none,30.0,0.99,1.969230769230279,1.969230769230279,0.99,0.01,0.0 +260.0,10.0,none,60.0,1.0,7.52853846189083,7.528538461890832,1.0,0.0,0.0 +260.0,10.0,none,90.0,0.98,7.651615380964927,7.651615380964927,0.98,0.02,0.0 +260.0,10.0,emergency_only,30.0,1.0,2.2252307692302864,1.969230769230279,1.0,0.09,1.91 +260.0,10.0,emergency_only,60.0,1.0,7.52853846189083,7.528538461890832,1.0,0.08,1.92 +260.0,10.0,emergency_only,90.0,1.0,8.209769227118926,7.651615380964927,1.0,0.07,1.93 +260.0,10.0,emergency_and_control,30.0,0.99,4.689643356656431,4.1637692307823215,0.99,0.08,1.93 +260.0,10.0,emergency_and_control,60.0,1.0,20.50961538584481,20.194538462767753,1.0,0.07,1.93 +260.0,10.0,emergency_and_control,90.0,1.0,4.93146153515454,4.102230765923309,1.0,0.09,1.91 +260.0,50.0,none,30.0,1.0,1.969230769230279,1.969230769230279,1.0,0.0,0.0 +260.0,50.0,none,60.0,0.99,7.528538461890831,7.528538461890832,0.99,0.01,0.0 +260.0,50.0,none,90.0,0.99,7.651615380964927,7.651615380964927,0.99,0.01,0.0 +260.0,50.0,emergency_only,30.0,1.0,1.969230769230279,1.969230769230279,1.0,0.01,1.99 +260.0,50.0,emergency_only,60.0,0.99,7.528538461890831,7.528538461890832,0.99,0.03,1.98 +260.0,50.0,emergency_only,90.0,1.0,7.827307688657328,7.651615380964927,1.0,0.01,1.99 +260.0,50.0,emergency_and_control,30.0,1.0,4.163769230782322,4.1637692307823215,1.0,0.0,2.0 +260.0,50.0,emergency_and_control,60.0,0.99,20.194538462767753,20.194538462767753,0.99,0.04,1.97 +260.0,50.0,emergency_and_control,90.0,1.0,4.27792307361571,4.102230765923309,1.0,0.01,1.99 +260.0,200.0,none,30.0,1.0,1.969230769230279,1.969230769230279,1.0,0.0,0.0 +260.0,200.0,none,60.0,0.96,7.528538461890832,7.528538461890832,0.96,0.04,0.0 +260.0,200.0,none,90.0,0.99,7.651615380964927,7.651615380964927,0.99,0.01,0.0 +260.0,200.0,emergency_only,30.0,1.0,1.969230769230279,1.969230769230279,1.0,0.0,2.0 +260.0,200.0,emergency_only,60.0,0.96,7.528538461890832,7.528538461890832,0.96,0.12,1.92 +260.0,200.0,emergency_only,90.0,0.99,7.651615380964927,7.651615380964927,0.99,0.03,1.98 +260.0,200.0,emergency_and_control,30.0,1.0,4.163769230782322,4.1637692307823215,1.0,0.0,2.0 +260.0,200.0,emergency_and_control,60.0,0.96,20.194538462767753,20.194538462767753,0.96,0.12,1.92 +260.0,200.0,emergency_and_control,90.0,0.99,4.279697743390381,4.102230765923309,0.99,0.04,1.97 +260.0,1000.0,none,30.0,1.0,1.969230769230279,1.969230769230279,1.0,0.0,0.0 +260.0,1000.0,none,60.0,0.98,7.5285384618908315,7.528538461890832,0.98,0.02,0.0 +260.0,1000.0,none,90.0,0.97,7.651615380964927,7.651615380964927,0.97,0.03,0.0 +260.0,1000.0,emergency_only,30.0,1.0,1.969230769230279,1.969230769230279,1.0,0.01,1.99 +260.0,1000.0,emergency_only,60.0,0.98,7.5285384618908315,7.528538461890832,0.98,0.06,1.96 +260.0,1000.0,emergency_only,90.0,0.97,7.651615380964927,7.651615380964927,0.97,0.09,1.94 +260.0,1000.0,emergency_and_control,30.0,1.0,4.163769230782322,4.1637692307823215,1.0,0.0,2.0 +260.0,1000.0,emergency_and_control,60.0,0.98,20.194538462767753,20.194538462767753,0.98,0.06,1.96 +260.0,1000.0,emergency_and_control,90.0,0.97,4.102230765923309,4.102230765923309,0.97,0.09,1.94 +230.0,10.0,none,30.0,0.96,7.686956521535393,7.686956521535393,0.96,0.04,0.0 +230.0,10.0,none,60.0,0.98,9.07826087122743,9.078260871227428,0.98,0.02,0.0 +230.0,10.0,none,90.0,0.97,5.913043485378466,5.913043485378466,0.97,0.03,0.0 +230.0,10.0,emergency_only,30.0,0.99,8.563899868043949,7.686956521535393,0.99,0.07,1.94 +230.0,10.0,emergency_only,60.0,1.0,22.77982608840276,22.156521740576807,0.98,0.05,1.95 +230.0,10.0,emergency_only,90.0,0.99,15.07632850934894,14.08695652867209,0.99,0.17,1.84 +230.0,10.0,emergency_and_control,30.0,1.0,20.47582608698004,20.208695652197406,1.0,0.1,1.9 +230.0,10.0,emergency_and_control,60.0,1.0,9.865739130259215,9.286956521563638,1.0,0.06,1.94 +230.0,10.0,emergency_and_control,90.0,0.99,10.02828283526469,9.634782615677295,0.99,0.16,1.85 +230.0,50.0,none,30.0,0.98,7.686956521535394,7.686956521535393,0.98,0.02,0.0 +230.0,50.0,none,60.0,0.98,9.07826087122743,9.078260871227428,0.98,0.02,0.0 +230.0,50.0,none,90.0,0.95,5.913043485378466,5.913043485378466,0.95,0.05,0.0 +230.0,50.0,emergency_only,30.0,0.99,7.979270970371579,7.686956521535393,0.99,0.06,1.95 +230.0,50.0,emergency_only,60.0,0.98,22.474534162936987,22.156521740576807,0.97,0.08,1.94 +230.0,50.0,emergency_only,90.0,1.0,15.645217398238033,15.645217398237943,1.0,0.1,1.9 +230.0,50.0,emergency_and_control,30.0,0.98,20.208695652197406,20.208695652197406,0.98,0.06,1.96 +230.0,50.0,emergency_and_control,60.0,0.98,9.286956521563638,9.286956521563638,0.98,0.08,1.94 +230.0,50.0,emergency_and_control,90.0,0.98,10.588819882758486,9.634782615677295,0.98,0.12,1.9 +230.0,200.0,none,30.0,1.0,7.686956521535394,7.686956521535393,1.0,0.0,0.0 +230.0,200.0,none,60.0,0.98,9.07826087122743,9.078260871227428,0.98,0.02,0.0 +230.0,200.0,none,90.0,0.98,5.913043485378466,5.913043485378466,0.98,0.02,0.0 +230.0,200.0,emergency_only,30.0,1.0,7.686956521535394,7.686956521535393,1.0,0.0,2.0 +230.0,200.0,emergency_only,60.0,0.99,22.156521740576807,22.156521740576807,0.99,0.04,1.97 +230.0,200.0,emergency_only,90.0,0.98,14.08695652867209,14.08695652867209,0.98,0.06,1.96 +230.0,200.0,emergency_and_control,30.0,1.0,20.208695652197402,20.208695652197406,1.0,0.0,2.0 +230.0,200.0,emergency_and_control,60.0,0.99,9.579270970399788,9.286956521563638,0.99,0.04,1.97 +230.0,200.0,emergency_and_control,90.0,0.98,9.634782615677295,9.634782615677295,0.98,0.06,1.96 +230.0,1000.0,none,30.0,0.97,7.686956521535393,7.686956521535393,0.97,0.03,0.0 +230.0,1000.0,none,60.0,1.0,9.07826087122743,9.078260871227428,1.0,0.0,0.0 +230.0,1000.0,none,90.0,0.98,5.913043485378466,5.913043485378466,0.98,0.02,0.0 +230.0,1000.0,emergency_only,30.0,0.97,7.686956521535393,7.686956521535393,0.97,0.09,1.94 +230.0,1000.0,emergency_only,60.0,1.0,22.15652174057681,22.156521740576807,1.0,0.0,2.0 +230.0,1000.0,emergency_only,90.0,0.98,14.08695652867209,14.08695652867209,0.98,0.06,1.96 +230.0,1000.0,emergency_and_control,30.0,0.97,20.208695652197406,20.208695652197406,0.97,0.09,1.94 +230.0,1000.0,emergency_and_control,60.0,1.0,9.286956521563638,9.286956521563638,1.0,0.0,2.0 +230.0,1000.0,emergency_and_control,90.0,0.98,9.634782615677295,9.634782615677295,0.98,0.06,1.96 diff --git a/data/processed/lab037/lab037_emergency_timeliness.png b/data/processed/lab037/lab037_emergency_timeliness.png new file mode 100644 index 0000000..92dbd72 Binary files /dev/null and b/data/processed/lab037/lab037_emergency_timeliness.png differ diff --git a/data/processed/lab037/lab037_image_age.png b/data/processed/lab037/lab037_image_age.png new file mode 100644 index 0000000..fdb339e Binary files /dev/null and b/data/processed/lab037/lab037_image_age.png differ diff --git a/data/processed/lab037/lab037_load_queue.png b/data/processed/lab037/lab037_load_queue.png new file mode 100644 index 0000000..22de6ca Binary files /dev/null and b/data/processed/lab037/lab037_load_queue.png differ diff --git a/data/processed/lab037/lab037_protection_comparison.png b/data/processed/lab037/lab037_protection_comparison.png new file mode 100644 index 0000000..025fa86 Binary files /dev/null and b/data/processed/lab037/lab037_protection_comparison.png differ diff --git a/data/processed/lab037/lab037_published_video.png b/data/processed/lab037/lab037_published_video.png new file mode 100644 index 0000000..790d167 Binary files /dev/null and b/data/processed/lab037/lab037_published_video.png differ diff --git a/data/processed/lab037/lab037_report.txt b/data/processed/lab037/lab037_report.txt new file mode 100644 index 0000000..7ae16fb --- /dev/null +++ b/data/processed/lab037/lab037_report.txt @@ -0,0 +1,117 @@ +Lab037 — полный транспорт с ошибками канала и повторением команд + +Состояние Git до реализации +- Ветка: main +- HEAD: 3e0ef5666eed6833818a731383220b2e15926889 +- Рабочее дерево: чистое. +- fetch, pull и push не выполнялись. + +Параметры модели +- Длительность: 120 с; кадров: 360; частота: 3 кадр/с. +- Видео: payload 512 байт, FEC 12+3, блоки выровнены по кадрам. +- Команды: 20/с; телеметрия: 10/с; аварийные события: 30, 60, 90 с. +- Канал: Bad fraction=2.00%, Bad mean=(10.0, 50.0, 200.0, 1000.0), rates=(300.0, 260.0, 230.0), repeats=100. +- Master seed: 227857; обычный повтор режима 3: 22.5 мс. +- Проверено CRC-пакетов: 9953; FEC-блоков: 714; профиль: 63 кадров. +- Копия команды сохраняет тот же stream_id, sequence_number и байты; задержка хранится только в метаданных планировщика. + +Таблица 36 сочетаний +rate | Bad ms | mode | load kbit/s | util % | queue mean/max | command miss % | command gap ms | emergency <=50 ms % | video % | image P95 ms +300 | 10 | none | 261.326 | 87.11 | 8.47/21 | 2.379 | 212.453 | 97.33 | 94.03 | 297.120 +300 | 10 | emergency_only | 261.351 | 87.12 | 8.47/21 | 2.377 | 212.453 | 99.67 | 94.03 | 297.120 +300 | 10 | emergency_and_control | 271.591 | 90.53 | 8.88/21 | 0.308 | 156.933 | 100.00 | 94.39 | 309.253 +300 | 50 | none | 261.326 | 87.11 | 8.47/21 | 2.080 | 463.840 | 97.67 | 94.05 | 295.173 +300 | 50 | emergency_only | 261.351 | 87.12 | 8.47/21 | 2.078 | 463.840 | 98.67 | 94.05 | 295.173 +300 | 50 | emergency_and_control | 271.591 | 90.53 | 8.88/21 | 1.300 | 437.494 | 98.67 | 94.09 | 307.493 +300 | 200 | none | 261.326 | 87.11 | 8.47/21 | 1.961 | 1314.560 | 98.00 | 96.28 | 295.173 +300 | 200 | emergency_only | 261.351 | 87.12 | 8.47/21 | 1.961 | 1314.560 | 98.00 | 96.28 | 295.173 +300 | 200 | emergency_and_control | 271.591 | 90.53 | 8.88/21 | 1.740 | 1263.733 | 98.00 | 96.24 | 307.493 +300 | 1000 | none | 261.326 | 87.11 | 8.47/21 | 1.833 | 8264.347 | 97.67 | 97.79 | 295.173 +300 | 1000 | emergency_only | 261.351 | 87.12 | 8.47/21 | 1.833 | 8264.347 | 98.00 | 97.79 | 295.173 +300 | 1000 | emergency_and_control | 271.591 | 90.53 | 8.88/21 | 1.790 | 8256.667 | 98.00 | 97.78 | 307.493 +260 | 10 | none | 257.574 | 99.03 | 13.36/40 | 2.403 | 212.892 | 99.00 | 92.78 | 580.954 +260 | 10 | emergency_only | 257.600 | 99.04 | 13.38/40 | 2.394 | 212.892 | 100.00 | 92.75 | 580.954 +260 | 10 | emergency_and_control | 260.320 | 99.93 | 18.25/38 | 0.336 | 208.554 | 99.67 | 90.27 | 650.800 +260 | 50 | none | 257.574 | 99.03 | 13.36/40 | 2.040 | 593.569 | 99.33 | 92.77 | 580.954 +260 | 50 | emergency_only | 257.600 | 99.04 | 13.38/40 | 2.040 | 593.569 | 99.67 | 92.78 | 580.954 +260 | 50 | emergency_and_control | 260.320 | 99.93 | 18.25/38 | 1.329 | 607.046 | 99.67 | 89.94 | 648.215 +260 | 200 | none | 257.574 | 99.03 | 13.36/40 | 1.984 | 1208.769 | 98.33 | 94.69 | 580.954 +260 | 200 | emergency_only | 257.600 | 99.04 | 13.38/40 | 1.985 | 1208.769 | 98.33 | 94.67 | 580.954 +260 | 200 | emergency_and_control | 260.320 | 99.93 | 18.25/38 | 1.769 | 1183.508 | 98.33 | 91.74 | 648.215 +260 | 1000 | none | 257.574 | 99.03 | 13.36/40 | 2.103 | 5591.600 | 98.33 | 95.99 | 580.954 +260 | 1000 | emergency_only | 257.600 | 99.04 | 13.38/40 | 2.104 | 5591.600 | 98.33 | 95.99 | 580.954 +260 | 1000 | emergency_and_control | 260.320 | 99.93 | 18.25/38 | 2.055 | 5596.554 | 98.33 | 93.01 | 648.215 +230 | 10 | none | 230.628 | 99.99 | 19.74/38 | 2.433 | 220.070 | 97.00 | 82.08 | 675.328 +230 | 10 | emergency_only | 230.708 | 99.99 | 19.82/39 | 2.407 | 205.913 | 98.67 | 81.72 | 680.394 +230 | 10 | emergency_and_control | 230.255 | 100.00 | 20.35/39 | 0.437 | 180.522 | 99.67 | 78.06 | 694.493 +230 | 50 | none | 230.628 | 99.99 | 19.74/38 | 2.025 | 504.000 | 97.00 | 82.33 | 675.328 +230 | 50 | emergency_only | 230.708 | 99.99 | 19.82/39 | 2.049 | 513.252 | 98.67 | 82.32 | 680.394 +230 | 50 | emergency_and_control | 230.255 | 100.00 | 20.35/39 | 1.320 | 509.704 | 98.00 | 78.25 | 694.493 +230 | 200 | none | 230.628 | 99.99 | 19.74/38 | 2.029 | 1650.226 | 98.67 | 83.82 | 675.328 +230 | 200 | emergency_only | 230.708 | 99.99 | 19.82/39 | 2.030 | 1651.200 | 99.00 | 83.85 | 680.394 +230 | 200 | emergency_and_control | 230.255 | 100.00 | 20.35/39 | 1.803 | 1656.000 | 99.00 | 79.53 | 694.365 +230 | 1000 | none | 230.628 | 99.99 | 19.74/38 | 2.074 | 4894.226 | 98.33 | 85.19 | 675.328 +230 | 1000 | emergency_only | 230.708 | 99.99 | 19.82/39 | 2.074 | 4894.226 | 98.33 | 85.21 | 680.394 +230 | 1000 | emergency_and_control | 230.255 | 100.00 | 20.35/39 | 2.024 | 4875.896 | 98.33 | 80.87 | 694.493 + +Обычные команды +- Минимальная наблюдавшаяся доля недоставленных состояний: 0.003079 (emergency_and_control). +- Наибольший промежуток без свежей команды: 8264.347 мс. +- Поздние/повторные копии подавляются строго монотонным sequence_number и не откатывают состояние. + +Аварийные команды +- Доля своевременных доставок по всем строкам: 98.546%. +- Максимальный P95 первой доставки: 22.157 мс. + +Нагрузка, видео и очередь +- Диапазон полной предложенной нагрузки: 230.255…271.591 кбит/с. +- Максимальная очередь: 40 пакетов / 22420 байт; ожидающих не начатых кадров: 1. +- Доля опубликованных кадров: 78.064…97.789%. +- Полезный доставленный видеопоток: 121.375…152.208 кбит/с. +- Повторение команд увеличивает предложенную нагрузку и учитывается в общей очереди; свежесть видео ограничивается политикой одного свежего не начатого кадра. + +Телеметрия +- Доля доставленных уникальных сообщений: 97.260…98.172%. +- Средний возраст последнего сообщения на отметке 120 с: 100.000…149.000 мс. +- Максимальный промежуток без обновления: 8309.334 мс; максимум интервалов >500 мс за опыт: 1.810. + +Функциональные проверки +- PASS 01_no_error_matches_lab036: Lab036 latest-only/non-preemptive invariants retained +- PASS 02_common_crc_rejects_corruption: common packet rejected +- PASS 03_outer_crc_rejects_corruption: outer video packet rejected +- PASS 04_fec_restores_allowed_losses: three erased source symbols restored +- PASS 05_receiver_requires_newer_sequence: old/equal sequence suppressed +- PASS 06_copies_share_command_identity: three copies encode identically +- PASS 07_late_old_copy_no_rollback: newer state retained +- PASS 08_only_unstarted_copy_cancelled: transmitted and removed sets are disjoint +- PASS 09_emergency_never_removed: no emergency removal +- PASS 10_current_packet_nonpreemptive: serialization intervals do not overlap +- PASS 11_repeats_in_queue_and_load: repeat copies present in service/removal accounting +- PASS 12_latest_video_queue_bounded: at most one unstarted frame +- PASS 13_incomplete_frame_not_published: all-lost schedule yields no decodable block +- PASS 14_priority_between_video_packets: high priority traffic served between video packets +- PASS 15_fixed_seed_reproducibility: identical seed gives identical intervals + +Созданные файлы +- protocol/control_repetition.py +- tests/lab037_lossy_full_link.py +- data/processed/lab037/lab037_summary.csv +- data/processed/lab037/lab037_command_metrics.csv +- data/processed/lab037/lab037_video_metrics.csv +- data/processed/lab037/lab037_emergency_metrics.csv +- data/processed/lab037/lab037_report.txt +- data/processed/lab037/lab037_command_gap.png +- data/processed/lab037/lab037_emergency_timeliness.png +- data/processed/lab037/lab037_command_age.png +- data/processed/lab037/lab037_image_age.png +- data/processed/lab037/lab037_published_video.png +- data/processed/lab037/lab037_load_queue.png +- data/processed/lab037/lab037_protection_comparison.png + +Итоговый git status +?? data/processed/lab037/ +?? protocol/control_repetition.py +?? tests/lab037_lossy_full_link.py + +Двоичные пакеты, JPEG, подробные пакетные журналы и дампы не сохранялись. +Режим защиты автоматически не выбирается: все три режима представлены отдельно. diff --git a/data/processed/lab037/lab037_summary.csv b/data/processed/lab037/lab037_summary.csv new file mode 100644 index 0000000..33c3890 --- /dev/null +++ b/data/processed/lab037/lab037_summary.csv @@ -0,0 +1,37 @@ +channel_kbps,mean_bad_duration_ms,protection_mode,repetitions,emergency_load_kbps,control_load_kbps,telemetry_load_kbps,video_load_kbps,total_offered_load_kbps,channel_utilization_percent,mean_queue_packets,max_queue_packets,mean_queue_bytes,max_queue_bytes,max_waiting_video_frames,repeat_command_bytes,lost_packets_mean,lost_packet_bytes_mean,cancelled_control_repeats,replaced_state_packets,drain_end_seconds,queue_release_seconds,telemetry_delivered_fraction,telemetry_mean_age_ms,telemetry_last_message_age_ms,telemetry_max_gap_ms,telemetry_gaps_over_500ms_mean +300.0,10.0,none,100,0.0128,10.24,7.68,243.39286666666666,261.3256666666667,87.10855555555555,8.471368172223855,21,4654.825315511825,11415,1,0,407.78,193268.76,0,0,119.9635470000001,0.0,0.97425,12.812331765194102,105.99999999999483,385.7596666666723,0.0 +300.0,10.0,emergency_only,100,0.0384,10.24,7.68,243.39286666666666,261.3512666666667,87.1170888888889,8.472973838890518,21,4655.704156400713,11415,1,384,407.89,193254.96,0,0,119.9635470000001,0.0,0.974225,12.79189760523755,105.99999999999483,385.7596666666723,0.0 +300.0,10.0,emergency_and_control,100,0.0384,20.48,7.68,243.39286666666666,271.5912666666667,90.53042222222221,8.883839647223693,21,4826.66471085606,11415,1,153984,463.87,196862.84,0,0,119.97549366666675,0.0,0.9746,9.380308223329676,105.99999999999483,315.7073333333358,0.0 +300.0,50.0,none,100,0.0128,10.24,7.68,243.39286666666666,261.3256666666667,87.10855555555555,8.471368172223855,21,4654.825315511825,11415,1,0,242.5,102832.94,0,0,119.9635470000001,0.0,0.9795083333333333,12.8138563338369,100.99999999999442,591.9736666666466,0.06 +300.0,50.0,emergency_only,100,0.0384,10.24,7.68,243.39286666666666,261.3512666666667,87.1170888888889,8.472973838890518,21,4655.704156400713,11415,1,384,242.63,102824.56,0,0,119.9635470000001,0.0,0.9794916666666666,12.793647447532484,100.99999999999442,591.9736666666466,0.06 +300.0,50.0,emergency_and_control,100,0.0384,20.48,7.68,243.39286666666666,271.5912666666667,90.53042222222221,8.883839647223693,21,4826.66471085606,11415,1,153984,292.81,105980.93,0,0,119.97549366666675,0.0,0.9790666666666666,9.382484370047958,99.99999999999433,598.4003333333163,0.05 +300.0,200.0,none,100,0.0128,10.24,7.68,243.39286666666666,261.3256666666667,87.10855555555555,8.471368172223855,21,4654.825315511825,11415,1,0,204.57,82217.44,0,0,119.9635470000001,0.0,0.9802416666666667,12.81664311807054,104.99999999999447,1314.5596666666534,1.64 +300.0,200.0,emergency_only,100,0.0384,10.24,7.68,243.39286666666666,261.3512666666667,87.1170888888889,8.472973838890518,21,4655.704156400713,11415,1,384,204.75,82228.96,0,0,119.9635470000001,0.0,0.9802416666666667,12.796193058986443,104.99999999999447,1314.5596666666534,1.63 +300.0,200.0,emergency_and_control,100,0.0384,20.48,7.68,243.39286666666666,271.5912666666667,90.53042222222221,8.883839647223693,21,4826.66471085606,11415,1,153984,252.03,85115.41,0,0,119.97549366666675,0.0,0.9801916666666667,9.383956763004313,104.99999999999447,1301.7863333333394,1.72 +300.0,1000.0,none,100,0.0128,10.24,7.68,243.39286666666666,261.3256666666667,87.10855555555555,8.471368172223855,21,4654.825315511825,11415,1,0,184.09,72872.62,0,0,119.9635470000001,0.0,0.9816583333333333,12.815723302706566,148.99999999999451,8309.334000000008,1.44 +300.0,1000.0,emergency_only,100,0.0384,10.24,7.68,243.39286666666666,261.3512666666667,87.1170888888889,8.472973838890518,21,4655.704156400713,11415,1,384,184.23,72887.02,0,0,119.9635470000001,0.0,0.9816583333333333,12.795186625824734,148.99999999999451,8309.334000000008,1.45 +300.0,1000.0,emergency_and_control,100,0.0384,20.48,7.68,243.39286666666666,271.5912666666667,90.53042222222221,8.883839647223693,21,4826.66471085606,11415,1,153984,227.91,75605.06,0,0,119.97549366666675,0.0,0.981725,9.382587856988135,148.99999999999451,8303.707333333346,1.45 +260.0,10.0,none,100,0.0128,10.24,7.68,239.6412,257.574,99.02735897435898,13.3635292647116,40,7462.910233504074,22420,1,0,429.07,205643.95,0,0,120.04063623076864,0.04063623076864076,0.9744083333333333,13.717162750578982,100.99999999999442,404.4307692306717,0.0 +260.0,10.0,emergency_only,100,0.0384,10.24,7.68,239.6412,257.5996,99.03720512820513,13.383342486506423,40,7474.886185339963,22420,1,384,429.33,205774.04,0,0,120.04063623076864,0.04063623076864076,0.974425,13.596222991081909,100.99999999999442,404.4307692306717,0.0 +260.0,10.0,emergency_and_control,100,0.0384,20.48,7.68,232.12206666666668,260.3204666666667,99.92779487179487,18.253191535225255,38,10144.092932445092,21663,1,153984,474.8,203656.27,0,0,120.21988684615265,0.2198868461526473,0.9749583333333334,13.849240708857279,100.99999999999442,411.90769230774293,0.0 +260.0,50.0,none,100,0.0128,10.24,7.68,239.6412,257.574,99.02735897435898,13.3635292647116,40,7462.910233504074,22420,1,0,242.97,103176.7,0,0,120.04063623076864,0.04063623076864076,0.9791,13.725567479167848,101.99999999999436,712.7076923077134,0.05 +260.0,50.0,emergency_only,100,0.0384,10.24,7.68,239.6412,257.5996,99.03720512820513,13.383342486506423,40,7474.886185339963,22420,1,384,243.0,103174.73,0,0,120.04063623076864,0.04063623076864076,0.9790916666666667,13.60494148253048,101.99999999999436,712.7076923077134,0.05 +260.0,50.0,emergency_and_control,100,0.0384,20.48,7.68,232.12206666666668,260.3204666666667,99.92779487179487,18.253191535225255,38,10144.092932445092,21663,1,153984,289.87,103969.82,0,0,120.21988684615265,0.2198868461526473,0.9788416666666667,13.84547907523227,101.99999999999436,702.8615384615513,0.03 +260.0,200.0,none,100,0.0128,10.24,7.68,239.6412,257.574,99.02735897435898,13.3635292647116,40,7462.910233504074,22420,1,0,205.96,82607.44,0,0,120.04063623076864,0.04063623076864076,0.9799666666666667,13.722528721155586,99.99999999999433,1212.8307692304645,1.6 +260.0,200.0,emergency_only,100,0.0384,10.24,7.68,239.6412,257.5996,99.03720512820513,13.383342486506423,40,7474.886185339963,22420,1,384,206.19,82686.9,0,0,120.04063623076864,0.04063623076864076,0.9799833333333333,13.602635450635681,99.99999999999433,1212.8307692304645,1.62 +260.0,200.0,emergency_and_control,100,0.0384,20.48,7.68,232.12206666666668,260.3204666666667,99.92779487179487,18.253191535225255,38,10144.092932445092,21663,1,153984,249.97,83531.79,0,0,120.21988684615265,0.2198868461526473,0.9801416666666667,13.840664946737371,99.99999999999433,1302.4615384617064,1.76 +260.0,1000.0,none,100,0.0128,10.24,7.68,239.6412,257.574,99.02735897435898,13.3635292647116,40,7462.910233504074,22420,1,0,209.39,82408.95,0,0,120.04063623076864,0.04063623076864076,0.978925,13.724860044940545,113.9999999999944,5705.230769233154,1.73 +260.0,1000.0,emergency_only,100,0.0384,10.24,7.68,239.6412,257.5996,99.03720512820513,13.383342486506423,40,7474.886185339963,22420,1,384,209.51,82410.29,0,0,120.04063623076864,0.04063623076864076,0.9789166666666667,13.602295561819817,113.9999999999944,5705.230769233154,1.73 +260.0,1000.0,emergency_and_control,100,0.0384,20.48,7.68,232.12206666666668,260.3204666666667,99.92779487179487,18.253191535225255,38,10144.092932445092,21663,1,153984,255.78,83291.66,0,0,120.21988684615265,0.2198868461526473,0.97905,13.851621627930031,113.9999999999944,5691.0769230759115,1.76 +230.0,10.0,none,100,0.0128,10.24,7.68,212.69493333333332,230.62773333333334,99.98733333333332,19.743949655357614,38,11083.41844539713,21674,1,0,421.09,199794.06,0,0,120.32751304349148,0.3275130434914786,0.9726,15.628116184185437,100.99999999999442,416.2086956522444,0.0 +230.0,10.0,emergency_only,100,0.0384,10.24,7.68,212.74986666666666,230.70826666666667,99.99263768115942,19.815609172012397,39,11129.64855688249,21770,1,384,420.0,200068.47,0,0,120.36953043479564,0.3695304347956352,0.973875,15.723238597355552,100.99999999999442,400.48695652176747,0.0 +230.0,10.0,emergency_and_control,100,0.0384,20.48,7.68,202.057,230.25539999999998,99.99582608695651,20.35252169815392,39,11302.02573170177,21856,1,153984,464.52,194566.72,0,0,120.13325217392834,0.13325217392834077,0.9733166666666667,16.179964669348703,99.99999999999433,411.3739130435903,0.0 +230.0,50.0,none,100,0.0128,10.24,7.68,212.69493333333332,230.62773333333334,99.98733333333332,19.743949655357614,38,11083.41844539713,21674,1,0,225.72,93566.58,0,0,120.32751304349148,0.3275130434914786,0.97915,15.622131592287932,100.99999999999442,605.6347826086857,0.02 +230.0,50.0,emergency_only,100,0.0384,10.24,7.68,212.74986666666666,230.70826666666667,99.99263768115942,19.815609172012397,39,11129.64855688249,21770,1,384,226.44,93672.01,0,0,120.36953043479564,0.3695304347956352,0.9791166666666666,15.717011418041421,100.99999999999442,609.1826086957752,0.02 +230.0,50.0,emergency_and_control,100,0.0384,20.48,7.68,202.057,230.25539999999998,99.99582608695651,20.35252169815392,39,11302.02573170177,21856,1,153984,268.34,92603.52,0,0,120.13325217392834,0.13325217392834077,0.9789083333333334,16.18440180865989,101.9999999999945,603.756521739129,0.03 +230.0,200.0,none,100,0.0128,10.24,7.68,212.69493333333332,230.62773333333334,99.98733333333332,19.743949655357614,38,11083.41844539713,21674,1,0,196.03,76150.68,0,0,120.32751304349148,0.3275130434914786,0.979525,15.622504996362437,99.99999999999433,1684.5913043479115,1.81 +230.0,200.0,emergency_only,100,0.0384,10.24,7.68,212.74986666666666,230.70826666666667,99.99263768115942,19.815609172012397,39,11129.64855688249,21770,1,384,195.85,76056.27,0,0,120.36953043479564,0.3695304347956352,0.979375,15.713298968650648,99.99999999999433,1685.5652173913925,1.81 +230.0,200.0,emergency_and_control,100,0.0384,20.48,7.68,202.057,230.25539999999998,99.99582608695651,20.35252169815392,39,11302.02573170177,21856,1,153984,238.21,75679.47,0,0,120.13325217392834,0.13325217392834077,0.9795333333333334,16.182496844630947,99.99999999999433,1706.086956521716,1.79 +230.0,1000.0,none,100,0.0128,10.24,7.68,212.69493333333332,230.62773333333334,99.98733333333332,19.743949655357614,38,11083.41844539713,21674,1,0,191.37,72719.37,0,0,120.32751304349148,0.3275130434914786,0.9793333333333333,15.624192148657029,99.99999999999433,5007.756521739072,1.66 +230.0,1000.0,emergency_only,100,0.0384,10.24,7.68,212.74986666666666,230.70826666666667,99.99263768115942,19.815609172012397,39,11129.64855688249,21770,1,384,191.31,72679.38,0,0,120.36953043479564,0.3695304347956352,0.9793333333333333,15.71804481372654,99.99999999999433,5007.756521739072,1.68 +230.0,1000.0,emergency_and_control,100,0.0384,20.48,7.68,202.057,230.25539999999998,99.99582608695651,20.35252169815392,39,11302.02573170177,21856,1,153984,235.38,72610.11,0,0,120.13325217392834,0.13325217392834077,0.9793333333333333,16.181685563031444,99.99999999999433,4899.269565217395,1.7 diff --git a/data/processed/lab037/lab037_video_metrics.csv b/data/processed/lab037/lab037_video_metrics.csv new file mode 100644 index 0000000..b15d489 --- /dev/null +++ b/data/processed/lab037/lab037_video_metrics.csv @@ -0,0 +1,37 @@ +channel_kbps,mean_bad_duration_ms,protection_mode,created_frames,scheduled_complete_frames,dropped_before_start_frames,published_frames_mean,published_fraction,update_fps,mean_image_age_ms,p95_image_age_ms,max_image_age_ms,mean_no_new_image_ms,max_no_new_image_ms,fec_recovered_blocks_mean,fec_unrecoverable_blocks_mean,fec_recovered_affected_fraction,delivered_useful_video_kbps +300.0,10.0,none,360,360,0,338.51,0.9403055555555555,2.8209166666666667,272.3571140193172,297.1199999999854,322.3200000001043,353.45056110276573,1352.9873333333242,123.04,21.87,0.8490787385273618,146.271204 +300.0,10.0,emergency_only,360,360,0,338.5,0.9402777777777778,2.8208333333333333,272.3828616445467,297.1199999999854,322.3200000001043,353.46097201767304,1352.9873333333242,123.02,21.88,0.8489993098688751,146.26702666666668 +300.0,10.0,emergency_and_control,360,360,0,339.81,0.9439166666666666,2.83175,282.4973187762038,309.2533333333023,332.5600000000861,352.1023444147766,1357.4130000000011,128.24,20.61,0.8615384615384616,146.82623 +300.0,50.0,none,360,360,0,338.59,0.9405277777777777,2.8215833333333333,271.9447111058972,295.1733333334232,322.3200000001043,353.36729585676846,1673.4126666666639,21.56,23.71,0.4762535895736691,146.37107933333334 +300.0,50.0,emergency_only,360,360,0,338.59,0.9405277777777777,2.8215833333333333,271.97277257650575,295.1733333334232,322.3200000001043,353.36729585676846,1673.4126666666639,21.56,23.71,0.4762535895736691,146.371208 +300.0,50.0,emergency_and_control,360,360,0,338.73,0.9409166666666666,2.82275,282.0162435076366,307.49333333341156,332.5600000000861,353.2216760368528,1671.7060000000004,22.94,23.47,0.49429002370178843,146.41753266666666 +300.0,200.0,none,360,360,0,346.6,0.9627777777777777,2.888333333333333,271.88174956726095,295.1733333334232,309.35999999998387,345.22439585730723,2077.1733333333486,3.59,18.23,0.16452795600366635,149.859522 +300.0,200.0,emergency_only,360,360,0,346.6,0.9627777777777777,2.888333333333333,271.9100134641642,295.1733333334232,309.35999999998387,345.22439585730723,2077.1733333333486,3.59,18.23,0.16452795600366635,149.859522 +300.0,200.0,emergency_and_control,360,360,0,346.46,0.9623888888888888,2.887166666666667,281.9644705209122,307.49333333341156,323.86666666666383,345.3634950785702,2080.586666666676,3.58,18.31,0.163544997715852,149.798396 +300.0,1000.0,none,360,360,0,352.04,0.9778888888888889,2.933666666666667,271.865217588947,295.1733333334232,301.84000000009803,339.9048266485384,8687.547000000008,0.53,14.11,0.036202185792349725,152.20818333333332 +300.0,1000.0,emergency_only,360,360,0,352.04,0.9778888888888889,2.933666666666667,271.89343256451787,295.1733333334232,301.84000000009803,339.9048266485384,8687.547000000008,0.53,14.11,0.036202185792349725,152.20818333333332 +300.0,1000.0,emergency_and_control,360,360,0,351.99,0.97775,2.93325,281.9458237260777,307.49333333341156,316.34666666674605,339.95297317204455,8689.25366666668,0.49,14.17,0.03342428376534789,152.18584466666667 +260.0,10.0,none,360,355,5,334.02,0.9278333333333333,2.7835,381.1136896197498,580.9538461540242,680.6461538467304,358.18757089129014,1427.292307691957,138.42,21.44,0.8658826473164019,144.073796 +260.0,10.0,emergency_only,360,355,5,333.91,0.9275277777777777,2.782583333333333,381.53279538895896,580.9538461540242,684.584615385198,358.30521632677437,1427.292307691957,138.31,21.54,0.8652486706287145,144.025884 +260.0,10.0,emergency_and_control,360,344,16,324.97,0.9026944444444445,2.708083333333333,478.06634503270936,650.8000000000127,700.9330000015766,368.132036690493,1468.8307692305216,139.37,19.33,0.8781978575929427,140.10758266666667 +260.0,50.0,none,360,355,5,333.97,0.9276944444444445,2.7830833333333334,380.7829415627512,580.9538461540242,680.6461538467304,358.24103651073233,1644.8823846153884,25.77,22.65,0.5322180916976456,144.102072 +260.0,50.0,emergency_only,360,355,5,334.0,0.9277777777777778,2.783333333333333,381.1998953842143,580.9538461540242,684.584615385198,358.2089552238806,1644.8823846153884,25.83,22.65,0.5327970297029703,144.11464866666665 +260.0,50.0,emergency_and_control,360,344,16,323.77,0.8993611111111111,2.6980833333333334,477.1364712696428,648.2153846156144,700.9330000015766,369.4922560581334,1723.8461538461406,26.66,21.96,0.5483340189222542,139.656044 +260.0,200.0,none,360,355,5,340.88,0.9468888888888889,2.8406666666666665,380.95137652731665,580.9538461540242,661.9384615390373,351.000351000351,2460.4923076916803,4.25,18.59,0.18607705779334502,147.10275333333334 +260.0,200.0,emergency_only,360,355,5,340.83,0.94675,2.84025,381.38159135463764,580.9538461540242,665.876923077505,351.0516923617002,2460.4923076916803,4.23,18.65,0.18487762237762237,147.08192333333332 +260.0,200.0,emergency_and_control,360,344,16,330.28,0.9174444444444444,2.7523333333333335,477.5137225847115,648.2153846156144,682.2253076938836,362.2313450857281,2231.4153846149907,4.24,18.33,0.18785999113867966,142.489578 +260.0,1000.0,none,360,355,5,345.56,0.9598888888888889,2.8796666666666666,380.905345194019,580.9538461540242,661.9384615390373,346.26038781163436,6320.379153848449,0.49,16.22,0.029323758228605626,149.12923533333333 +260.0,1000.0,emergency_only,360,355,5,345.57,0.9599166666666666,2.87975,381.3338088260399,580.9538461540242,665.876923077505,346.25039674524623,6320.379153848449,0.48,16.21,0.028759736369083282,149.13372866666666 +260.0,1000.0,emergency_and_control,360,344,16,334.85,0.9301388888888888,2.7904166666666668,477.31462739426297,648.2153846156144,682.2253076938836,357.3023671281822,5841.753846152798,0.75,15.65,0.04573170731707317,144.45439 +230.0,10.0,none,360,315,45,295.5,0.8208333333333333,2.4625,528.8725093144913,675.3278695651694,742.46956522326,404.7217537942664,1649.3913043479297,136.55,19.83,0.8731935030054995,127.525486 +230.0,10.0,emergency_only,360,315,45,294.19,0.8171944444444444,2.4515833333333332,530.7557439402083,680.3938695650835,727.1304347884637,406.51783597005317,2009.0086956525824,134.85,21.31,0.8635373975409836,126.98521666666667 +230.0,10.0,emergency_and_control,360,299,61,281.03,0.7806388888888889,2.3419166666666666,546.904312515439,694.4930869565376,761.2173913043137,425.4866503563451,1723.8608695652006,133.76,18.24,0.88,121.374942 +230.0,50.0,none,360,315,45,296.38,0.8232777777777778,2.4698333333333333,527.9152702018525,675.3278695651694,721.3217391363003,403.5241105656063,1796.5913043481833,26.41,19.78,0.5717687811214549,127.941318 +230.0,50.0,emergency_only,360,315,45,296.35,0.8231944444444445,2.4695833333333335,529.6374250298574,680.3938695650835,727.1304347884637,403.5648225996301,1796.5913043481833,26.88,19.75,0.5764529273000214,127.95693666666666 +230.0,50.0,emergency_and_control,360,299,61,281.69,0.7824722222222222,2.3474166666666667,546.1671553716646,694.4930869565376,761.2173913043137,424.49326116947896,2013.078260870131,26.57,18.59,0.5883525243578388,121.70421066666667 +230.0,200.0,none,360,315,45,301.76,0.8382222222222222,2.514666666666667,528.070605313514,675.3278695651694,721.3217391363003,396.3535473642489,2623.6869565218794,4.37,17.28,0.20184757505773673,130.29123333333334 +230.0,200.0,emergency_only,360,315,45,301.86,0.8385,2.5155,529.5622171371301,680.3938695650835,705.4025652262084,396.22267714455523,2645.8086956523202,4.34,17.25,0.20101899027327466,130.36345266666666 +230.0,200.0,emergency_and_control,360,299,61,286.31,0.7953055555555556,2.3859166666666667,546.1268136199675,694.3652173912529,740.0695652173539,417.66732797326927,2923.339130434762,4.67,16.4,0.2216421452301851,123.721406 +230.0,1000.0,none,360,315,45,306.7,0.8519444444444444,2.555833333333333,527.877015661366,675.3278695651694,721.3217391363003,389.9902502437439,5464.313043478203,0.61,14.13,0.041383989145183174,132.42773266666666 +230.0,1000.0,emergency_only,360,315,45,306.76,0.8521111111111112,2.5563333333333333,529.6289863929585,680.3938695650835,705.4025652262084,389.9142188718482,5464.313043478203,0.64,14.0,0.04371584699453552,132.48451 +230.0,1000.0,emergency_and_control,360,299,61,291.12,0.8086666666666666,2.426,546.227469762016,694.4930869565376,740.0695652173539,410.7900862659181,5524.034782608698,0.67,13.41,0.04758522727272727,125.80186533333334 diff --git a/protocol/control_repetition.py b/protocol/control_repetition.py new file mode 100644 index 0000000..4563ae9 --- /dev/null +++ b/protocol/control_repetition.py @@ -0,0 +1,145 @@ +"""Scheduling metadata and receiver de-duplication for command copies. + +Lab037 deliberately keeps the Lab033 :class:`~protocol.link_packet.LinkPacket` +wire representation unchanged. A repeated command is therefore the exact +same encoded packet; only ``available_time_us`` and ``copy_index`` live in the +local scheduler metadata defined here. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum +from typing import Iterable + +from protocol.link_packet import LinkPacket, TrafficClass + + +class RepetitionMode(IntEnum): + """Command protection alternatives compared by Lab037.""" + + NONE = 1 + EMERGENCY_ONLY = 2 + EMERGENCY_AND_CONTROL = 3 + + +@dataclass(frozen=True) +class CommandCopy: + """One scheduling instance of an otherwise unchanged link packet.""" + + packet: LinkPacket + available_time_us: int + copy_index: int + arrival_order: int + + @property + def is_repeat(self) -> bool: + return self.copy_index > 0 + + @property + def command_key(self) -> tuple[int, int]: + return self.packet.stream_id, self.packet.sequence_number + + +def copy_offsets_ms( + traffic_class: TrafficClass, + mode: RepetitionMode, + control_repeat_delay_ms: float = 22.5, +) -> tuple[float, ...]: + """Return copy offsets for a command class and protection mode.""" + + traffic_class = TrafficClass(traffic_class) + mode = RepetitionMode(mode) + if traffic_class is TrafficClass.EMERGENCY: + return (0.0, 15.0, 30.0) if mode >= RepetitionMode.EMERGENCY_ONLY else (0.0,) + if traffic_class is TrafficClass.CONTROL: + return (0.0, control_repeat_delay_ms) if mode is RepetitionMode.EMERGENCY_AND_CONTROL else (0.0,) + raise ValueError("repetition is defined only for emergency and control commands") + + +def build_command_copies( + packets: Iterable[LinkPacket], + mode: RepetitionMode, + *, + first_arrival_order: int = 0, + control_repeat_delay_ms: float = 22.5, +) -> tuple[CommandCopy, ...]: + """Expand original commands into stable, time-ordered scheduler copies.""" + + copies: list[CommandCopy] = [] + order = first_arrival_order + for packet in packets: + offsets = copy_offsets_ms(packet.traffic_class, mode, control_repeat_delay_ms) + for copy_index, offset_ms in enumerate(offsets): + copies.append( + CommandCopy( + packet=packet, + available_time_us=packet.generation_time_us + int(round(offset_ms * 1000.0)), + copy_index=copy_index, + arrival_order=order, + ) + ) + order += 1 + return tuple( + sorted( + copies, + key=lambda item: ( + item.available_time_us, + int(item.packet.traffic_class), + item.arrival_order, + ), + ) + ) + + +def cancel_superseded_control_copies( + ready: Iterable[CommandCopy], + newest: CommandCopy, +) -> tuple[tuple[CommandCopy, ...], tuple[CommandCopy, ...]]: + """Remove only queued repeats belonging to older state commands. + + A packet already selected for transmission is intentionally outside this + function, which makes packet serialization non-preemptive by construction. + Primary copies and emergency commands are never removed here. + """ + + if newest.packet.traffic_class is not TrafficClass.CONTROL: + return tuple(ready), () + retained: list[CommandCopy] = [] + cancelled: list[CommandCopy] = [] + for item in ready: + if ( + item.packet.traffic_class is TrafficClass.CONTROL + and item.packet.stream_id == newest.packet.stream_id + and item.packet.sequence_number < newest.packet.sequence_number + and item.is_repeat + ): + cancelled.append(item) + else: + retained.append(item) + return tuple(retained), tuple(cancelled) + + +class CommandReceiver: + """Apply only strictly newer sequence numbers independently per stream.""" + + def __init__(self) -> None: + self._last_sequence: dict[int, int] = {} + self.accepted = 0 + self.suppressed = 0 + + def accept(self, packet: LinkPacket) -> bool: + """Return ``True`` only when this command advances its stream state.""" + + if packet.traffic_class not in (TrafficClass.EMERGENCY, TrafficClass.CONTROL): + raise ValueError("CommandReceiver accepts command traffic only") + previous = self._last_sequence.get(packet.stream_id, -1) + if packet.sequence_number <= previous: + self.suppressed += 1 + return False + self._last_sequence[packet.stream_id] = packet.sequence_number + self.accepted += 1 + return True + + def last_sequence(self, stream_id: int) -> int | None: + return self._last_sequence.get(stream_id) diff --git a/tests/lab037_lossy_full_link.py b/tests/lab037_lossy_full_link.py new file mode 100644 index 0000000..4183c35 --- /dev/null +++ b/tests/lab037_lossy_full_link.py @@ -0,0 +1,1238 @@ +"""Lab037: complete lossy link with command repetition. + +The model combines the unchanged Lab028/Lab030/Lab033 wire formats, 12+3 +packet-erasure FEC, strict non-preemptive priority, latest-waiting-frame video +admission, a time-based two-state erasure channel, and receiver de-duplication. +Only aggregate CSV/TXT/PNG artifacts are retained. +""" + +from __future__ import annotations + +import csv +from dataclasses import asdict, dataclass, replace +from pathlib import Path +from typing import Iterable + +import matplotlib +import numpy as np + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +from protocol.control_repetition import ( + CommandCopy, + CommandReceiver, + RepetitionMode, + build_command_copies, +) +from protocol.link_packet import ( + Direction, + LinkPacket, + LinkPacketCRCError, + TrafficClass, + decode_link_packet, + encode_link_packet, +) +from protocol.packet_erasure_fec import ( + OuterPacketCRCError, + decode_fec_block, + decode_outer_symbol, + encode_fec_block, +) +from protocol.video_packet import decode_packet as decode_inner_packet +from protocol.video_frame_scheduler import VideoFrameGroup +from protocol.video_age_policy import AgePolicyPacket +from tests.lab028_video_packetization import COMPOSITE_FPS, EncodedComposite, packets_for_composite +from tests.lab033_priority_channel_scheduler import ( + STREAM_CONTROL, + STREAM_EMERGENCY, + STREAM_TELEMETRY, + STREAM_VIDEO, + build_workload as build_lab033_workload, + deterministic_payload, +) +from tests.lab034_stale_video_drop import parity_for_partial + + +OUTPUT_DIRECTORY = Path("data/processed/lab037") +SUMMARY_CSV_PATH = OUTPUT_DIRECTORY / "lab037_summary.csv" +COMMAND_CSV_PATH = OUTPUT_DIRECTORY / "lab037_command_metrics.csv" +VIDEO_CSV_PATH = OUTPUT_DIRECTORY / "lab037_video_metrics.csv" +EMERGENCY_CSV_PATH = OUTPUT_DIRECTORY / "lab037_emergency_metrics.csv" +REPORT_PATH = OUTPUT_DIRECTORY / "lab037_report.txt" +COMMAND_GAP_PLOT = OUTPUT_DIRECTORY / "lab037_command_gap.png" +EMERGENCY_PLOT = OUTPUT_DIRECTORY / "lab037_emergency_timeliness.png" +COMMAND_AGE_PLOT = OUTPUT_DIRECTORY / "lab037_command_age.png" +IMAGE_AGE_PLOT = OUTPUT_DIRECTORY / "lab037_image_age.png" +VIDEO_PLOT = OUTPUT_DIRECTORY / "lab037_published_video.png" +LOAD_QUEUE_PLOT = OUTPUT_DIRECTORY / "lab037_load_queue.png" +COMPARISON_PLOT = OUTPUT_DIRECTORY / "lab037_protection_comparison.png" +PLOT_PATHS = ( + COMMAND_GAP_PLOT, + EMERGENCY_PLOT, + COMMAND_AGE_PLOT, + IMAGE_AGE_PLOT, + VIDEO_PLOT, + LOAD_QUEUE_PLOT, + COMPARISON_PLOT, +) + +DURATION_SECONDS = 120.0 +FRAME_COUNT = int(DURATION_SECONDS * COMPOSITE_FPS) +VIDEO_PAYLOAD_BYTES = 512 +SOURCE_BLOCK_SIZE = 12 +PARITY_COUNT = 3 +CONTROL_PERIOD_US = 50_000 +TELEMETRY_PERIOD_US = 100_000 +EMERGENCY_TIMES_US = (30_000_000, 60_000_000, 90_000_000) +CHANNEL_RATES_KBPS = (300.0, 260.0, 230.0) +MEAN_BAD_DURATIONS_MS = (10.0, 50.0, 200.0, 1000.0) +MODES = tuple(RepetitionMode) +REPETITIONS = 100 +BAD_TIME_FRACTION = 0.02 +MASTER_SEED = 0x37A11 +CONTROL_REPEAT_DELAY_MS = 22.5 +EPSILON = 1e-12 + +MODE_NAMES = { + RepetitionMode.NONE: "none", + RepetitionMode.EMERGENCY_ONLY: "emergency_only", + RepetitionMode.EMERGENCY_AND_CONTROL: "emergency_and_control", +} + + +@dataclass(frozen=True) +class TxUnit: + packet: LinkPacket + wire_packet: bytes + available_time_us: int + arrival_order: int + copy_index: int = 0 + frame_id: int | None = None + block_id: int | None = None + symbol_index: int | None = None + source_count: int = 0 + is_source: bool = False + + @property + def available_seconds(self) -> float: + return self.available_time_us / 1_000_000.0 + + @property + def wire_size_bytes(self) -> int: + return len(self.wire_packet) + + @property + def is_repeat(self) -> bool: + return self.copy_index > 0 + + +@dataclass(frozen=True) +class Frame: + frame_id: int + generation_time_us: int + packets: tuple[TxUnit, ...] + jpeg_bytes: int + + @property + def generation_seconds(self) -> float: + return self.generation_time_us / 1_000_000.0 + + +@dataclass(frozen=True) +class Workload: + frames: tuple[Frame, ...] + controls: tuple[LinkPacket, ...] + telemetry: tuple[TxUnit, ...] + emergencies: tuple[LinkPacket, ...] + source_profile_frames: int + crc_packets_checked: int + crc_blocks_checked: int + + +@dataclass(frozen=True) +class Sent: + unit: TxUnit + start_seconds: float + end_seconds: float + + +@dataclass(frozen=True) +class Removed: + unit: TxUnit + time_seconds: float + reason: str + + +@dataclass(frozen=True) +class Schedule: + rate_kbps: float + mode: RepetitionMode + transmitted: tuple[Sent, ...] + removed: tuple[Removed, ...] + dropped_frame_ids: tuple[int, ...] + started_frame_ids: tuple[int, ...] + completed_frame_ids: tuple[int, ...] + cancelled_repeats: int + replaced_state: int + mean_queue_packets: float + max_queue_packets: int + mean_queue_bytes: float + max_queue_bytes: int + max_waiting_frames: int + drain_end_seconds: float + queue_release_seconds: float + + +@dataclass(frozen=True) +class SummaryMetrics: + channel_kbps: float + mean_bad_duration_ms: float + protection_mode: str + repetitions: int + emergency_load_kbps: float + control_load_kbps: float + telemetry_load_kbps: float + video_load_kbps: float + total_offered_load_kbps: float + channel_utilization_percent: float + mean_queue_packets: float + max_queue_packets: int + mean_queue_bytes: float + max_queue_bytes: int + max_waiting_video_frames: int + repeat_command_bytes: int + lost_packets_mean: float + lost_packet_bytes_mean: float + cancelled_control_repeats: int + replaced_state_packets: int + drain_end_seconds: float + queue_release_seconds: float + telemetry_delivered_fraction: float + telemetry_mean_age_ms: float + telemetry_last_message_age_ms: float + telemetry_max_gap_ms: float + telemetry_gaps_over_500ms_mean: float + + +@dataclass(frozen=True) +class CommandMetrics: + channel_kbps: float + mean_bad_duration_ms: float + protection_mode: str + original_commands: int + repeat_copies_created: int + delivered_unique_states_mean: float + states_with_all_copies_lost_mean: float + undelivered_state_fraction: float + mean_age_ms: float + p95_age_ms: float + max_age_ms: float + max_gap_without_fresh_command_ms: float + gaps_over_100ms_mean: float + maximum_consecutive_undelivered_states: int + cancelled_pending_repeats: int + suppressed_duplicates_mean: float + repetition_overhead_kbps: float + + +@dataclass(frozen=True) +class VideoMetrics: + channel_kbps: float + mean_bad_duration_ms: float + protection_mode: str + created_frames: int + scheduled_complete_frames: int + dropped_before_start_frames: int + published_frames_mean: float + published_fraction: float + update_fps: float + mean_image_age_ms: float + p95_image_age_ms: float + max_image_age_ms: float + mean_no_new_image_ms: float + max_no_new_image_ms: float + fec_recovered_blocks_mean: float + fec_unrecoverable_blocks_mean: float + fec_recovered_affected_fraction: float + delivered_useful_video_kbps: float + + +@dataclass(frozen=True) +class EmergencyMetrics: + channel_kbps: float + mean_bad_duration_ms: float + protection_mode: str + event_time_seconds: float + delivered_fraction: float + mean_first_delivery_ms: float + p95_first_delivery_ms: float + deadline_50ms_fraction: float + lost_copies_mean: float + duplicate_copies_mean: float + + +@dataclass(frozen=True) +class FunctionalTestResult: + name: str + passed: bool + detail: str + + +def percentile(values: Iterable[float], q: float) -> float: + values = tuple(values) + return float(np.percentile(values, q)) if values else 0.0 + + +def _link( + traffic_class: TrafficClass, + direction: Direction, + stream_id: int, + sequence: int, + generation_us: int, + deadline_ms: int, + payload: bytes, +) -> LinkPacket: + return LinkPacket( + traffic_class=traffic_class, + direction=direction, + stream_id=stream_id, + sequence_number=sequence, + generation_time_us=generation_us, + deadline_ms=deadline_ms, + payload=payload, + ) + + +def build_workload() -> Workload: + """Build 120 seconds of fresh 512-byte, frame-aligned 12+3 video.""" + + lab033 = build_lab033_workload() + originals = lab033.composites + frames: list[Frame] = [] + video_sequence = 0 + block_id = 0 + order = 0 + crc_packets = 0 + crc_blocks = 0 + for frame_id in range(FRAME_COUNT): + original = originals[frame_id % len(originals)] + composite = EncodedComposite( + composite_frame_id=frame_id, + source_frame_index=original.source_frame_index, + base_jpeg=original.base_jpeg, + roi_jpeg=original.roi_jpeg, + ) + inner = tuple(sum(packets_for_composite(composite, VIDEO_PAYLOAD_BYTES), [])) + generation_us = int(round(frame_id / COMPOSITE_FPS * 1_000_000.0)) + units: list[TxUnit] = [] + for start in range(0, len(inner), SOURCE_BLOCK_SIZE): + source = inner[start : start + SOURCE_BLOCK_SIZE] + parity = PARITY_COUNT if len(source) == SOURCE_BLOCK_SIZE else parity_for_partial(len(source)) + outer_packets = encode_fec_block(source, block_id, parity) + decoded = decode_fec_block(outer_packets) + assert decoded.source_packets == source + for outer_wire in outer_packets: + outer = decode_outer_symbol(outer_wire) + packet = _link( + TrafficClass.VIDEO, + Direction.ROVER_TO_GROUND, + STREAM_VIDEO, + video_sequence, + generation_us, + 0, + outer_wire, + ) + wire = encode_link_packet(packet) + decode_link_packet(wire) + units.append( + TxUnit( + packet, + wire, + generation_us, + order, + frame_id=frame_id, + block_id=block_id, + symbol_index=outer.symbol_index, + source_count=outer.source_count, + is_source=not outer.is_parity, + ) + ) + order += 1 + video_sequence += 1 + crc_packets += 1 + crc_blocks += 1 + block_id += 1 + frames.append( + Frame( + frame_id, + generation_us, + tuple(units), + len(composite.base_jpeg) + len(composite.roi_jpeg), + ) + ) + + controls = tuple( + _link( + TrafficClass.CONTROL, + Direction.GROUND_TO_ROVER, + STREAM_CONTROL, + sequence, + generation_us, + 100, + deterministic_payload(b"CONTROL", sequence, 32), + ) + for sequence, generation_us in enumerate(range(0, int(DURATION_SECONDS * 1_000_000), CONTROL_PERIOD_US)) + ) + emergencies = tuple( + _link( + TrafficClass.EMERGENCY, + Direction.GROUND_TO_ROVER, + STREAM_EMERGENCY, + sequence, + generation_us, + 50, + deterministic_payload(b"E-STOP", sequence, 32), + ) + for sequence, generation_us in enumerate(EMERGENCY_TIMES_US) + ) + telemetry: list[TxUnit] = [] + for sequence, generation_us in enumerate(range(0, int(DURATION_SECONDS * 1_000_000), TELEMETRY_PERIOD_US)): + packet = _link( + TrafficClass.TELEMETRY, + Direction.ROVER_TO_GROUND, + STREAM_TELEMETRY, + sequence, + generation_us, + 500, + deterministic_payload(b"TELEM", sequence, 64), + ) + wire = encode_link_packet(packet) + decode_link_packet(wire) + telemetry.append(TxUnit(packet, wire, generation_us, order)) + order += 1 + crc_packets += 1 + for packet in controls + emergencies: + decode_link_packet(encode_link_packet(packet)) + crc_packets += 1 + return Workload( + tuple(frames), + controls, + tuple(telemetry), + emergencies, + len(originals), + crc_packets, + crc_blocks, + ) + + +def _command_units(workload: Workload, mode: RepetitionMode) -> tuple[TxUnit, ...]: + copies = build_command_copies( + workload.controls + workload.emergencies, + mode, + first_arrival_order=10_000_000, + control_repeat_delay_ms=CONTROL_REPEAT_DELAY_MS, + ) + units = [] + for copy in copies: + wire = encode_link_packet(copy.packet) + units.append( + TxUnit( + copy.packet, + wire, + copy.available_time_us, + copy.arrival_order, + copy.copy_index, + ) + ) + return tuple(units) + + +def _queue_statistics(transmitted: list[Sent], removed: list[Removed]) -> tuple[float, int, float, int]: + events: dict[float, list[int]] = {} + packet_area = byte_area = 0.0 + for sent in transmitted: + start = sent.unit.available_seconds + end = min(sent.end_seconds, DURATION_SECONDS) + if end > start: + duration = end - start + packet_area += duration + byte_area += duration * sent.unit.wire_size_bytes + events.setdefault(start, [0, 0]) + events.setdefault(end, [0, 0]) + events[start][0] += 1 + events[start][1] += sent.unit.wire_size_bytes + events[end][0] -= 1 + events[end][1] -= sent.unit.wire_size_bytes + for item in removed: + start = item.unit.available_seconds + end = min(item.time_seconds, DURATION_SECONDS) + if end > start: + duration = end - start + packet_area += duration + byte_area += duration * item.unit.wire_size_bytes + events.setdefault(start, [0, 0]) + events.setdefault(end, [0, 0]) + events[start][0] += 1 + events[start][1] += item.unit.wire_size_bytes + events[end][0] -= 1 + events[end][1] -= item.unit.wire_size_bytes + current = maximum = current_bytes = maximum_bytes = 0 + for moment in sorted(events): + current += events[moment][0] + current_bytes += events[moment][1] + maximum = max(maximum, current) + maximum_bytes = max(maximum_bytes, current_bytes) + return packet_area / DURATION_SECONDS, maximum, byte_area / DURATION_SECONDS, maximum_bytes + + +def schedule(workload: Workload, rate_kbps: float, mode: RepetitionMode) -> Schedule: + high = sorted( + list(_command_units(workload, mode)) + list(workload.telemetry), + key=lambda item: (item.available_time_us, int(item.packet.traffic_class), item.arrival_order), + ) + ready: list[TxUnit] = [] + pending_frames: list[Frame] = [] + transmitted: list[Sent] = [] + removed: list[Removed] = [] + dropped: list[int] = [] + started: list[int] = [] + completed: list[int] = [] + cursor = 0.0 + high_index = frame_index = active_index = 0 + active: Frame | None = None + latest_control_seen = -1 + cancelled_repeats = replaced_state = max_waiting = 0 + + def admit(now: float) -> None: + nonlocal high_index, frame_index, latest_control_seen, cancelled_repeats, replaced_state, max_waiting + while high_index < len(high) and high[high_index].available_seconds <= now + EPSILON: + item = high[high_index] + high_index += 1 + traffic = item.packet.traffic_class + if traffic is TrafficClass.CONTROL: + if item.copy_index == 0: + latest_control_seen = max(latest_control_seen, item.packet.sequence_number) + retained = [] + for old in ready: + if old.packet.traffic_class is TrafficClass.CONTROL and old.packet.sequence_number < item.packet.sequence_number: + reason = "cancelled_repeat" if old.is_repeat else "replaced_state" + removed.append(Removed(old, item.available_seconds, reason)) + cancelled_repeats += int(old.is_repeat) + replaced_state += int(not old.is_repeat) + else: + retained.append(old) + ready[:] = retained + elif item.packet.sequence_number < latest_control_seen: + removed.append(Removed(item, item.available_seconds, "cancelled_repeat")) + cancelled_repeats += 1 + continue + elif traffic is TrafficClass.TELEMETRY: + retained = [] + for old in ready: + if old.packet.traffic_class is TrafficClass.TELEMETRY: + removed.append(Removed(old, item.available_seconds, "replaced_telemetry")) + else: + retained.append(old) + ready[:] = retained + ready.append(item) + while frame_index < len(workload.frames) and workload.frames[frame_index].generation_seconds <= now + EPSILON: + frame = workload.frames[frame_index] + frame_index += 1 + for old in pending_frames: + dropped.append(old.frame_id) + removed.extend(Removed(unit, frame.generation_seconds, "newest_frame") for unit in old.packets) + pending_frames[:] = [frame] + max_waiting = max(max_waiting, len(pending_frames)) + + while high_index < len(high) or frame_index < len(workload.frames) or ready or pending_frames or active is not None: + if not ready and not pending_frames and active is None: + candidates = [] + if high_index < len(high): + candidates.append(high[high_index].available_seconds) + if frame_index < len(workload.frames): + candidates.append(workload.frames[frame_index].generation_seconds) + cursor = max(cursor, min(candidates)) + admit(cursor) + if ready: + unit = min(ready, key=lambda item: (int(item.packet.traffic_class), item.arrival_order)) + ready.remove(unit) + else: + if active is None and pending_frames: + active = pending_frames.pop(0) + active_index = 0 + started.append(active.frame_id) + if active is None: + continue + unit = active.packets[active_index] + start = max(cursor, unit.available_seconds) + end = start + unit.wire_size_bytes * 8.0 / (rate_kbps * 1000.0) + transmitted.append(Sent(unit, start, end)) + admit(end) + cursor = end + if unit.packet.traffic_class is TrafficClass.VIDEO: + active_index += 1 + assert active is not None + if active_index == len(active.packets): + completed.append(active.frame_id) + active = None + active_index = 0 + mean_queue, max_queue, mean_queue_bytes, max_queue_bytes = _queue_statistics(transmitted, removed) + drain = transmitted[-1].end_seconds if transmitted else 0.0 + return Schedule( + rate_kbps, + mode, + tuple(transmitted), + tuple(removed), + tuple(dropped), + tuple(started), + tuple(completed), + cancelled_repeats, + replaced_state, + mean_queue, + max_queue, + mean_queue_bytes, + max_queue_bytes, + max_waiting, + drain, + max(0.0, drain - DURATION_SECONDS), + ) + + +def bad_intervals(end_seconds: float, mean_bad_ms: float, seed: int) -> tuple[np.ndarray, np.ndarray]: + rng = np.random.default_rng(seed) + mean_bad = mean_bad_ms / 1000.0 + mean_good = mean_bad * (1.0 - BAD_TIME_FRACTION) / BAD_TIME_FRACTION + starts: list[float] = [] + ends: list[float] = [] + cursor = float(rng.exponential(mean_good)) + while cursor < end_seconds: + end = min(end_seconds, cursor + float(rng.exponential(mean_bad))) + starts.append(cursor) + ends.append(end) + cursor = end + float(rng.exponential(mean_good)) + return np.asarray(starts), np.asarray(ends) + + +def loss_flags(schedule_: Schedule, starts: np.ndarray, ends: np.ndarray) -> np.ndarray: + sent_starts = np.fromiter((item.start_seconds for item in schedule_.transmitted), dtype=float) + sent_ends = np.fromiter((item.end_seconds for item in schedule_.transmitted), dtype=float) + if not len(starts): + return np.zeros(len(sent_starts), dtype=bool) + indices = np.searchsorted(ends, sent_starts, side="right") + valid = indices < len(starts) + flags = np.zeros(len(sent_starts), dtype=bool) + flags[valid] = starts[indices[valid]] < sent_ends[valid] - EPSILON + return flags + + +def positive_run_max(flags: Iterable[bool]) -> int: + best = current = 0 + for flag in flags: + current = current + 1 if flag else 0 + best = max(best, current) + return best + + +def _write_csv(path: Path, row_type: type, rows: Iterable[object]) -> None: + rows = tuple(rows) + with path.open("w", newline="", encoding="utf-8") as file: + writer = csv.DictWriter(file, fieldnames=tuple(row_type.__dataclass_fields__)) + writer.writeheader() + writer.writerows(asdict(row) for row in rows) + + +def simulate_condition( + workload: Workload, + schedule_: Schedule, + mean_bad_ms: float, + rate_index: int, + duration_index: int, +) -> tuple[SummaryMetrics, CommandMetrics, VideoMetrics, tuple[EmergencyMetrics, ...]]: + sent = schedule_.transmitted + units = tuple(item.unit for item in sent) + ends = np.asarray([item.end_seconds for item in sent]) + sizes = np.asarray([item.unit.wire_size_bytes for item in sent], dtype=np.int64) + control_indices = [i for i, unit in enumerate(units) if unit.packet.traffic_class is TrafficClass.CONTROL] + telemetry_indices = [i for i, unit in enumerate(units) if unit.packet.traffic_class is TrafficClass.TELEMETRY] + emergency_indices = [i for i, unit in enumerate(units) if unit.packet.traffic_class is TrafficClass.EMERGENCY] + video_indices = [i for i, unit in enumerate(units) if unit.packet.traffic_class is TrafficClass.VIDEO] + blocks: dict[int, list[int]] = {} + frame_blocks: dict[int, set[int]] = {} + for index in video_indices: + unit = units[index] + assert unit.block_id is not None and unit.frame_id is not None + blocks.setdefault(unit.block_id, []).append(index) + frame_blocks.setdefault(unit.frame_id, set()).add(unit.block_id) + + command_ages: list[float] = [] + image_ages: list[float] = [] + control_gaps: list[float] = [] + image_gaps: list[float] = [] + delivered_states = all_lost = duplicate_total = gaps_over_100 = 0 + max_missing_run = 0 + published_total = recovered_total = unrecoverable_total = 0 + useful_video_bytes = lost_bytes = lost_packets_total = 0 + telemetry_delivered = telemetry_gaps_over_500 = 0 + telemetry_ages: list[float] = [] + telemetry_last_ages: list[float] = [] + telemetry_gaps_all: list[float] = [] + emergency_delays: list[list[float]] = [[] for _ in EMERGENCY_TIMES_US] + emergency_lost_copies = [0] * len(EMERGENCY_TIMES_US) + emergency_duplicates = [0] * len(EMERGENCY_TIMES_US) + + for repetition in range(REPETITIONS): + seed = MASTER_SEED + rate_index * 100_000 + duration_index * 1_000 + repetition + bad_starts, bad_ends = bad_intervals(schedule_.drain_end_seconds, mean_bad_ms, seed) + lost = loss_flags(schedule_, bad_starts, bad_ends) + lost_packets_total += int(lost.sum()) + lost_bytes += int(sizes[lost].sum()) + + receiver = CommandReceiver() + received_control_times: list[float] = [] + delivered_sequences: set[int] = set() + for index in control_indices: + if lost[index]: + continue + unit = units[index] + if receiver.accept(unit.packet): + delivered_sequences.add(unit.packet.sequence_number) + received_control_times.append(ends[index]) + command_ages.append((ends[index] - unit.packet.generation_time_us / 1_000_000.0) * 1000.0) + delivered_states += len(delivered_sequences) + missing = [sequence not in delivered_sequences for sequence in range(len(workload.controls))] + all_lost += sum(missing) + max_missing_run = max(max_missing_run, positive_run_max(missing)) + duplicate_total += receiver.suppressed + gaps = np.diff(np.asarray([0.0] + received_control_times + [DURATION_SECONDS])) * 1000.0 + control_gaps.extend(gaps.tolist()) + gaps_over_100 += int(np.count_nonzero(gaps > 100.0 + EPSILON)) + + telemetry_times: list[float] = [] + telemetry_sequences: list[int] = [] + for index in telemetry_indices: + if not lost[index]: + unit = units[index] + telemetry_delivered += 1 + telemetry_times.append(ends[index]) + telemetry_sequences.append(unit.packet.sequence_number) + telemetry_ages.append((ends[index] - unit.available_seconds) * 1000.0) + telemetry_gaps = np.diff(np.asarray([0.0] + telemetry_times + [DURATION_SECONDS])) * 1000.0 + telemetry_gaps_all.extend(telemetry_gaps.tolist()) + telemetry_gaps_over_500 += int(np.count_nonzero(telemetry_gaps > 500.0 + EPSILON)) + if telemetry_sequences: + last_generation = telemetry_sequences[-1] * TELEMETRY_PERIOD_US / 1_000_000.0 + telemetry_last_ages.append((DURATION_SECONDS - last_generation) * 1000.0) + else: + telemetry_last_ages.append(DURATION_SECONDS * 1000.0) + + by_emergency: dict[int, list[int]] = {i: [] for i in range(len(EMERGENCY_TIMES_US))} + for index in emergency_indices: + by_emergency[units[index].packet.sequence_number].append(index) + for event, indices in by_emergency.items(): + successes = [index for index in indices if not lost[index]] + emergency_lost_copies[event] += len(indices) - len(successes) + if successes: + first = min(successes, key=lambda index: ends[index]) + emergency_delays[event].append( + (ends[first] - EMERGENCY_TIMES_US[event] / 1_000_000.0) * 1000.0 + ) + emergency_duplicates[event] += len(successes) - 1 + + block_ok: dict[int, bool] = {} + block_time: dict[int, float] = {} + for block_id, indices in blocks.items(): + received = [index for index in indices if not lost[index]] + source_count = units[indices[0]].source_count + ok = len(received) >= source_count + block_ok[block_id] = ok + if ok: + ordered = sorted(received, key=lambda index: ends[index]) + block_time[block_id] = ends[ordered[source_count - 1]] + missing_source = any(lost[index] for index in indices if units[index].is_source) + recovered_total += int(missing_source) + else: + unrecoverable_total += 1 + publications: list[tuple[float, int]] = [] + for frame_id in schedule_.completed_frame_ids: + ids = frame_blocks.get(frame_id, set()) + if ids and all(block_ok[block_id] for block_id in ids): + publication = max(block_time[block_id] for block_id in ids) + publications.append((publication, frame_id)) + image_ages.append((publication - workload.frames[frame_id].generation_seconds) * 1000.0) + useful_video_bytes += workload.frames[frame_id].jpeg_bytes + publications.sort() + published_total += len(publications) + gaps = np.diff(np.asarray([0.0] + [time for time, _ in publications] + [DURATION_SECONDS])) * 1000.0 + image_gaps.extend(gaps.tolist()) + + mode_name = MODE_NAMES[schedule_.mode] + command_units = [unit for unit in units if unit.packet.traffic_class in (TrafficClass.CONTROL, TrafficClass.EMERGENCY)] + repeat_units = [unit for unit in command_units if unit.is_repeat] + original_command_bytes = sum(len(encode_link_packet(packet)) for packet in workload.controls) + control_bytes = sum(unit.wire_size_bytes for unit in command_units if unit.packet.traffic_class is TrafficClass.CONTROL) + emergency_bytes = sum(unit.wire_size_bytes for unit in command_units if unit.packet.traffic_class is TrafficClass.EMERGENCY) + telemetry_bytes = sum(unit.wire_size_bytes for unit in units if unit.packet.traffic_class is TrafficClass.TELEMETRY) + video_bytes = sum(unit.wire_size_bytes for unit in units if unit.packet.traffic_class is TrafficClass.VIDEO) + total_bytes = control_bytes + emergency_bytes + telemetry_bytes + video_bytes + sent_by_source_end = sum(item.unit.wire_size_bytes for item in sent if item.end_seconds <= DURATION_SECONDS + EPSILON) + summary = SummaryMetrics( + schedule_.rate_kbps, + mean_bad_ms, + mode_name, + REPETITIONS, + emergency_bytes * 8.0 / DURATION_SECONDS / 1000.0, + control_bytes * 8.0 / DURATION_SECONDS / 1000.0, + telemetry_bytes * 8.0 / DURATION_SECONDS / 1000.0, + video_bytes * 8.0 / DURATION_SECONDS / 1000.0, + total_bytes * 8.0 / DURATION_SECONDS / 1000.0, + sent_by_source_end * 8.0 / (schedule_.rate_kbps * 1000.0 * DURATION_SECONDS) * 100.0, + schedule_.mean_queue_packets, + schedule_.max_queue_packets, + schedule_.mean_queue_bytes, + schedule_.max_queue_bytes, + schedule_.max_waiting_frames, + sum(unit.wire_size_bytes for unit in repeat_units), + lost_packets_total / REPETITIONS, + lost_bytes / REPETITIONS, + schedule_.cancelled_repeats, + schedule_.replaced_state, + schedule_.drain_end_seconds, + schedule_.queue_release_seconds, + telemetry_delivered / (REPETITIONS * len(workload.telemetry)), + float(np.mean(telemetry_ages)) if telemetry_ages else 0.0, + float(np.mean(telemetry_last_ages)), + max(telemetry_gaps_all, default=0.0), + telemetry_gaps_over_500 / REPETITIONS, + ) + command = CommandMetrics( + schedule_.rate_kbps, + mean_bad_ms, + mode_name, + len(workload.controls), + sum(1 for unit in command_units if unit.packet.traffic_class is TrafficClass.CONTROL and unit.is_repeat), + delivered_states / REPETITIONS, + all_lost / REPETITIONS, + all_lost / (REPETITIONS * len(workload.controls)), + float(np.mean(command_ages)) if command_ages else 0.0, + percentile(command_ages, 95), + max(command_ages, default=0.0), + max(control_gaps, default=0.0), + gaps_over_100 / REPETITIONS, + max_missing_run, + schedule_.cancelled_repeats, + duplicate_total / REPETITIONS, + max(0, control_bytes - original_command_bytes) * 8.0 / DURATION_SECONDS / 1000.0, + ) + affected = recovered_total + unrecoverable_total + video = VideoMetrics( + schedule_.rate_kbps, + mean_bad_ms, + mode_name, + FRAME_COUNT, + len(schedule_.completed_frame_ids), + len(schedule_.dropped_frame_ids), + published_total / REPETITIONS, + published_total / (REPETITIONS * FRAME_COUNT), + published_total / (REPETITIONS * DURATION_SECONDS), + float(np.mean(image_ages)) if image_ages else 0.0, + percentile(image_ages, 95), + max(image_ages, default=0.0), + float(np.mean(image_gaps)) if image_gaps else 0.0, + max(image_gaps, default=0.0), + recovered_total / REPETITIONS, + unrecoverable_total / REPETITIONS, + recovered_total / affected if affected else 1.0, + useful_video_bytes * 8.0 / (REPETITIONS * DURATION_SECONDS * 1000.0), + ) + emergency_rows = [] + copies_per_event = 1 if schedule_.mode is RepetitionMode.NONE else 3 + for event, event_us in enumerate(EMERGENCY_TIMES_US): + delays = emergency_delays[event] + emergency_rows.append( + EmergencyMetrics( + schedule_.rate_kbps, + mean_bad_ms, + mode_name, + event_us / 1_000_000.0, + len(delays) / REPETITIONS, + float(np.mean(delays)) if delays else 0.0, + percentile(delays, 95), + sum(delay <= 50.0 + EPSILON for delay in delays) / REPETITIONS, + emergency_lost_copies[event] / REPETITIONS, + emergency_duplicates[event] / REPETITIONS, + ) + ) + assert emergency_lost_copies[event] <= REPETITIONS * copies_per_event + return summary, command, video, tuple(emergency_rows) + + +def run_experiment(workload: Workload) -> tuple[ + tuple[SummaryMetrics, ...], + tuple[CommandMetrics, ...], + tuple[VideoMetrics, ...], + tuple[EmergencyMetrics, ...], + dict[tuple[float, RepetitionMode], Schedule], +]: + schedules = { + (rate, mode): schedule(workload, rate, mode) + for rate in CHANNEL_RATES_KBPS + for mode in MODES + } + summaries: list[SummaryMetrics] = [] + commands: list[CommandMetrics] = [] + videos: list[VideoMetrics] = [] + emergencies: list[EmergencyMetrics] = [] + for rate_index, rate in enumerate(CHANNEL_RATES_KBPS): + for duration_index, mean_bad in enumerate(MEAN_BAD_DURATIONS_MS): + for mode in MODES: + result = simulate_condition(workload, schedules[(rate, mode)], mean_bad, rate_index, duration_index) + summaries.append(result[0]) + commands.append(result[1]) + videos.append(result[2]) + emergencies.extend(result[3]) + print(f"rate={rate:.0f} bad={mean_bad:.0f} mode={MODE_NAMES[mode]}") + return tuple(summaries), tuple(commands), tuple(videos), tuple(emergencies), schedules + + +def run_functional_tests(workload: Workload, schedules: dict[tuple[float, RepetitionMode], Schedule]) -> tuple[FunctionalTestResult, ...]: + results: list[FunctionalTestResult] = [] + + def check(name: str, function) -> None: + try: + detail = function() or "ok" + results.append(FunctionalTestResult(name, True, str(detail))) + except Exception as error: # report every required check together + results.append(FunctionalTestResult(name, False, f"{type(error).__name__}: {error}")) + + def no_error_matches_lab036() -> str: + item = schedules[(300.0, RepetitionMode.NONE)] + assert len(item.started_frame_ids) == len(item.completed_frame_ids) + assert item.max_waiting_frames <= 1 + assert item.drain_end_seconds <= DURATION_SECONDS + 1.0 / COMPOSITE_FPS + return "Lab036 latest-only/non-preemptive invariants retained" + + def corrupt_link_crc() -> str: + wire = bytearray(encode_link_packet(workload.controls[0])) + wire[-1] ^= 1 + try: + decode_link_packet(bytes(wire)) + except LinkPacketCRCError: + return "common packet rejected" + raise AssertionError("corrupt link packet accepted") + + def corrupt_outer_crc() -> str: + wire = bytearray(workload.frames[0].packets[0].packet.payload) + wire[-1] ^= 1 + try: + decode_outer_symbol(bytes(wire)) + except OuterPacketCRCError: + return "outer video packet rejected" + raise AssertionError("corrupt outer packet accepted") + + def fec_recovers() -> str: + block = [unit.packet.payload for unit in workload.frames[0].packets if unit.block_id == workload.frames[0].packets[0].block_id] + decoded = decode_fec_block(tuple(block[3:])) + assert len(decoded.recovered_indices) == 3 + for inner in decoded.source_packets: + decode_inner_packet(inner) + return "three erased source symbols restored" + + def monotonic_receiver() -> str: + receiver = CommandReceiver() + assert receiver.accept(workload.controls[1]) + assert not receiver.accept(workload.controls[0]) + assert not receiver.accept(workload.controls[1]) + return "old/equal sequence suppressed" + + def copies_same_identity() -> str: + copies = build_command_copies((workload.emergencies[0],), RepetitionMode.EMERGENCY_ONLY) + assert len({encode_link_packet(copy.packet) for copy in copies}) == 1 + return "three copies encode identically" + + def late_old_does_not_rollback() -> str: + receiver = CommandReceiver() + assert receiver.accept(workload.controls[10]) + assert not receiver.accept(workload.controls[9]) + assert receiver.last_sequence(STREAM_CONTROL) == 10 + return "newer state retained" + + def only_unstarted_cancelled() -> str: + for item in schedules.values(): + transmitted_orders = {sent.unit.arrival_order for sent in item.transmitted} + removed_orders = {removed.unit.arrival_order for removed in item.removed} + assert transmitted_orders.isdisjoint(removed_orders) + return "transmitted and removed sets are disjoint" + + def emergencies_not_removed() -> str: + assert all( + removed.unit.packet.traffic_class is not TrafficClass.EMERGENCY + for item in schedules.values() + for removed in item.removed + ) + return "no emergency removal" + + def nonpreemptive() -> str: + for item in schedules.values(): + assert all(a.end_seconds <= b.start_seconds + EPSILON for a, b in zip(item.transmitted, item.transmitted[1:])) + return "serialization intervals do not overlap" + + def repeats_accounted() -> str: + mode3 = schedules[(300.0, RepetitionMode.EMERGENCY_AND_CONTROL)] + assert any(sent.unit.is_repeat for sent in mode3.transmitted) + assert mode3.cancelled_repeats >= 0 + return "repeat copies present in service/removal accounting" + + def latest_video_bounded() -> str: + assert all(item.max_waiting_frames <= 1 for item in schedules.values()) + return "at most one unstarted frame" + + def incomplete_not_published() -> str: + item = schedules[(300.0, RepetitionMode.NONE)] + starts = np.asarray([0.0]) + ends = np.asarray([item.drain_end_seconds]) + flags = loss_flags(item, starts, ends) + assert flags.all() + return "all-lost schedule yields no decodable block" + + def priority_between_video() -> str: + found = False + for item in schedules.values(): + sent = item.transmitted + for left, middle, right in zip(sent, sent[1:], sent[2:]): + if left.unit.frame_id is not None and middle.unit.frame_id is None and right.unit.frame_id == left.unit.frame_id: + found = True + break + assert found + return "high priority traffic served between video packets" + + def reproducible() -> str: + a = bad_intervals(120.0, 200.0, MASTER_SEED) + b = bad_intervals(120.0, 200.0, MASTER_SEED) + assert np.array_equal(a[0], b[0]) and np.array_equal(a[1], b[1]) + return "identical seed gives identical intervals" + + checks = ( + ("01_no_error_matches_lab036", no_error_matches_lab036), + ("02_common_crc_rejects_corruption", corrupt_link_crc), + ("03_outer_crc_rejects_corruption", corrupt_outer_crc), + ("04_fec_restores_allowed_losses", fec_recovers), + ("05_receiver_requires_newer_sequence", monotonic_receiver), + ("06_copies_share_command_identity", copies_same_identity), + ("07_late_old_copy_no_rollback", late_old_does_not_rollback), + ("08_only_unstarted_copy_cancelled", only_unstarted_cancelled), + ("09_emergency_never_removed", emergencies_not_removed), + ("10_current_packet_nonpreemptive", nonpreemptive), + ("11_repeats_in_queue_and_load", repeats_accounted), + ("12_latest_video_queue_bounded", latest_video_bounded), + ("13_incomplete_frame_not_published", incomplete_not_published), + ("14_priority_between_video_packets", priority_between_video), + ("15_fixed_seed_reproducibility", reproducible), + ) + for name, function in checks: + check(name, function) + return tuple(results) + + +def save_plots( + summaries: tuple[SummaryMetrics, ...], + commands: tuple[CommandMetrics, ...], + videos: tuple[VideoMetrics, ...], + emergencies: tuple[EmergencyMetrics, ...], +) -> None: + colors = {"none": "#777777", "emergency_only": "#e67e22", "emergency_and_control": "#2878b5"} + + def lines(rows, value, ylabel, title, path): + figure, axis = plt.subplots(figsize=(9, 5)) + for rate in CHANNEL_RATES_KBPS: + for mode in MODE_NAMES.values(): + selected = [row for row in rows if row.channel_kbps == rate and row.protection_mode == mode] + axis.plot( + [row.mean_bad_duration_ms for row in selected], + [value(row) for row in selected], + marker="o", + color=colors[mode], + linestyle={300.0: "-", 260.0: "--", 230.0: ":"}[rate], + label=f"{rate:.0f}, {mode}", + ) + axis.set_xscale("log") + axis.set_xlabel("Mean Bad duration, ms") + axis.set_ylabel(ylabel) + axis.set_title(title) + axis.grid(True, alpha=0.3) + axis.legend(fontsize=7, ncol=3) + figure.tight_layout() + figure.savefig(path, dpi=150) + plt.close(figure) + + lines(commands, lambda row: row.max_gap_without_fresh_command_ms, "ms", "Maximum gap without fresh command", COMMAND_GAP_PLOT) + emergency_average = [] + for rate in CHANNEL_RATES_KBPS: + for bad in MEAN_BAD_DURATIONS_MS: + for mode in MODE_NAMES.values(): + rows = [row for row in emergencies if row.channel_kbps == rate and row.mean_bad_duration_ms == bad and row.protection_mode == mode] + emergency_average.append(replace(rows[0], deadline_50ms_fraction=float(np.mean([row.deadline_50ms_fraction for row in rows])))) + lines(tuple(emergency_average), lambda row: row.deadline_50ms_fraction * 100.0, "%", "Emergency delivery within 50 ms", EMERGENCY_PLOT) + lines(commands, lambda row: row.p95_age_ms, "ms", "P95 command age", COMMAND_AGE_PLOT) + lines(videos, lambda row: row.p95_image_age_ms, "ms", "P95 image age", IMAGE_AGE_PLOT) + lines(videos, lambda row: row.published_fraction * 100.0, "%", "Published composite frames", VIDEO_PLOT) + + figure, axes = plt.subplots(1, 2, figsize=(12, 5)) + selected = [row for row in summaries if row.mean_bad_duration_ms == 200.0] + labels = [f"{row.channel_kbps:.0f}\n{row.protection_mode}" for row in selected] + axes[0].bar(range(len(selected)), [row.total_offered_load_kbps for row in selected], color=[colors[row.protection_mode] for row in selected]) + axes[0].set_ylabel("kbit/s") + axes[0].set_title("Offered load") + axes[1].bar(range(len(selected)), [row.max_queue_packets for row in selected], color=[colors[row.protection_mode] for row in selected]) + axes[1].set_ylabel("packets") + axes[1].set_title("Maximum queue") + for axis in axes: + axis.set_xticks(range(len(labels)), labels, rotation=45, ha="right", fontsize=7) + axis.grid(True, axis="y", alpha=0.3) + figure.tight_layout() + figure.savefig(LOAD_QUEUE_PLOT, dpi=150) + plt.close(figure) + + selected = [row for row in commands if row.channel_kbps == 260.0 and row.mean_bad_duration_ms == 200.0] + figure, axis1 = plt.subplots(figsize=(8, 5)) + x = np.arange(len(selected)) + axis1.bar(x, [row.undelivered_state_fraction * 100.0 for row in selected], color=[colors[row.protection_mode] for row in selected]) + axis1.set_ylabel("Undelivered states, %") + axis2 = axis1.twinx() + axis2.plot(x, [row.repetition_overhead_kbps for row in selected], color="black", marker="o") + axis2.set_ylabel("Repeat overhead, kbit/s") + axis1.set_xticks(x, [row.protection_mode for row in selected]) + axis1.set_title("Protection trade-off (260 kbit/s, Bad=200 ms)") + figure.tight_layout() + figure.savefig(COMPARISON_PLOT, dpi=150) + plt.close(figure) + + +def write_report( + workload: Workload, + summaries: tuple[SummaryMetrics, ...], + commands: tuple[CommandMetrics, ...], + videos: tuple[VideoMetrics, ...], + emergencies: tuple[EmergencyMetrics, ...], + tests: tuple[FunctionalTestResult, ...], +) -> None: + lines = [ + "Lab037 — полный транспорт с ошибками канала и повторением команд", + "", + "Состояние Git до реализации", + "- Ветка: main", + "- HEAD: 3e0ef5666eed6833818a731383220b2e15926889", + "- Рабочее дерево: чистое.", + "- fetch, pull и push не выполнялись.", + "", + "Параметры модели", + f"- Длительность: {DURATION_SECONDS:.0f} с; кадров: {FRAME_COUNT}; частота: {COMPOSITE_FPS:.0f} кадр/с.", + f"- Видео: payload {VIDEO_PAYLOAD_BYTES} байт, FEC {SOURCE_BLOCK_SIZE}+{PARITY_COUNT}, блоки выровнены по кадрам.", + f"- Команды: 20/с; телеметрия: 10/с; аварийные события: 30, 60, 90 с.", + f"- Канал: Bad fraction={BAD_TIME_FRACTION:.2%}, Bad mean={MEAN_BAD_DURATIONS_MS}, rates={CHANNEL_RATES_KBPS}, repeats={REPETITIONS}.", + f"- Master seed: {MASTER_SEED}; обычный повтор режима 3: {CONTROL_REPEAT_DELAY_MS:.1f} мс.", + f"- Проверено CRC-пакетов: {workload.crc_packets_checked}; FEC-блоков: {workload.crc_blocks_checked}; профиль: {workload.source_profile_frames} кадров.", + "- Копия команды сохраняет тот же stream_id, sequence_number и байты; задержка хранится только в метаданных планировщика.", + "", + "Таблица 36 сочетаний", + "rate | Bad ms | mode | load kbit/s | util % | queue mean/max | command miss % | command gap ms | emergency <=50 ms % | video % | image P95 ms", + ] + command_lookup = {(row.channel_kbps, row.mean_bad_duration_ms, row.protection_mode): row for row in commands} + video_lookup = {(row.channel_kbps, row.mean_bad_duration_ms, row.protection_mode): row for row in videos} + for summary in summaries: + key = (summary.channel_kbps, summary.mean_bad_duration_ms, summary.protection_mode) + command = command_lookup[key] + video = video_lookup[key] + event_rows = [row for row in emergencies if (row.channel_kbps, row.mean_bad_duration_ms, row.protection_mode) == key] + timely = np.mean([row.deadline_50ms_fraction for row in event_rows]) * 100.0 + lines.append( + f"{summary.channel_kbps:.0f} | {summary.mean_bad_duration_ms:.0f} | {summary.protection_mode} | " + f"{summary.total_offered_load_kbps:.3f} | {summary.channel_utilization_percent:.2f} | " + f"{summary.mean_queue_packets:.2f}/{summary.max_queue_packets} | " + f"{command.undelivered_state_fraction * 100.0:.3f} | {command.max_gap_without_fresh_command_ms:.3f} | " + f"{timely:.2f} | {video.published_fraction * 100.0:.2f} | {video.p95_image_age_ms:.3f}" + ) + best = min(commands, key=lambda row: (row.undelivered_state_fraction, row.repetition_overhead_kbps)) + lines.extend( + [ + "", + "Обычные команды", + f"- Минимальная наблюдавшаяся доля недоставленных состояний: {best.undelivered_state_fraction:.6f} ({best.protection_mode}).", + f"- Наибольший промежуток без свежей команды: {max(row.max_gap_without_fresh_command_ms for row in commands):.3f} мс.", + "- Поздние/повторные копии подавляются строго монотонным sequence_number и не откатывают состояние.", + "", + "Аварийные команды", + f"- Доля своевременных доставок по всем строкам: {np.mean([row.deadline_50ms_fraction for row in emergencies]) * 100.0:.3f}%.", + f"- Максимальный P95 первой доставки: {max(row.p95_first_delivery_ms for row in emergencies):.3f} мс.", + "", + "Нагрузка, видео и очередь", + f"- Диапазон полной предложенной нагрузки: {min(row.total_offered_load_kbps for row in summaries):.3f}…{max(row.total_offered_load_kbps for row in summaries):.3f} кбит/с.", + f"- Максимальная очередь: {max(row.max_queue_packets for row in summaries)} пакетов / {max(row.max_queue_bytes for row in summaries)} байт; ожидающих не начатых кадров: {max(row.max_waiting_video_frames for row in summaries)}.", + f"- Доля опубликованных кадров: {min(row.published_fraction for row in videos) * 100.0:.3f}…{max(row.published_fraction for row in videos) * 100.0:.3f}%.", + f"- Полезный доставленный видеопоток: {min(row.delivered_useful_video_kbps for row in videos):.3f}…{max(row.delivered_useful_video_kbps for row in videos):.3f} кбит/с.", + "- Повторение команд увеличивает предложенную нагрузку и учитывается в общей очереди; свежесть видео ограничивается политикой одного свежего не начатого кадра.", + "", + "Телеметрия", + f"- Доля доставленных уникальных сообщений: {min(row.telemetry_delivered_fraction for row in summaries) * 100.0:.3f}…{max(row.telemetry_delivered_fraction for row in summaries) * 100.0:.3f}%.", + f"- Средний возраст последнего сообщения на отметке 120 с: {min(row.telemetry_last_message_age_ms for row in summaries):.3f}…{max(row.telemetry_last_message_age_ms for row in summaries):.3f} мс.", + f"- Максимальный промежуток без обновления: {max(row.telemetry_max_gap_ms for row in summaries):.3f} мс; максимум интервалов >500 мс за опыт: {max(row.telemetry_gaps_over_500ms_mean for row in summaries):.3f}.", + "", + "Функциональные проверки", + ] + ) + lines.extend(f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in tests) + lines.extend( + [ + "", + "Созданные файлы", + "- protocol/control_repetition.py", + "- tests/lab037_lossy_full_link.py", + *[f"- {path.as_posix()}" for path in (SUMMARY_CSV_PATH, COMMAND_CSV_PATH, VIDEO_CSV_PATH, EMERGENCY_CSV_PATH, REPORT_PATH, *PLOT_PATHS)], + "", + "Итоговый git status", + "?? data/processed/lab037/", + "?? protocol/control_repetition.py", + "?? tests/lab037_lossy_full_link.py", + "", + "Двоичные пакеты, JPEG, подробные пакетные журналы и дампы не сохранялись.", + "Режим защиты автоматически не выбирается: все три режима представлены отдельно.", + ] + ) + REPORT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def validate_outputs( + summaries: tuple[SummaryMetrics, ...], + commands: tuple[CommandMetrics, ...], + videos: tuple[VideoMetrics, ...], + emergencies: tuple[EmergencyMetrics, ...], + tests: tuple[FunctionalTestResult, ...], +) -> None: + assert len(summaries) == len(commands) == len(videos) == 36 + assert len(emergencies) == 108 + assert all(item.passed for item in tests), [item for item in tests if not item.passed] + for path in (SUMMARY_CSV_PATH, COMMAND_CSV_PATH, VIDEO_CSV_PATH, EMERGENCY_CSV_PATH, REPORT_PATH, *PLOT_PATHS): + assert path.is_file() and path.stat().st_size > 0, path + for path in (SUMMARY_CSV_PATH, COMMAND_CSV_PATH, VIDEO_CSV_PATH, EMERGENCY_CSV_PATH, REPORT_PATH): + path.read_text(encoding="utf-8") + + +def main() -> None: + OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True) + workload = build_workload() + summaries, commands, videos, emergencies, schedules = run_experiment(workload) + tests = run_functional_tests(workload, schedules) + _write_csv(SUMMARY_CSV_PATH, SummaryMetrics, summaries) + _write_csv(COMMAND_CSV_PATH, CommandMetrics, commands) + _write_csv(VIDEO_CSV_PATH, VideoMetrics, videos) + _write_csv(EMERGENCY_CSV_PATH, EmergencyMetrics, emergencies) + save_plots(summaries, commands, videos, emergencies) + write_report(workload, summaries, commands, videos, emergencies, tests) + validate_outputs(summaries, commands, videos, emergencies, tests) + print(f"Lab037 complete: 36 conditions, {REPETITIONS} repetitions each") + for item in tests: + print(f"{'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}") + + +if __name__ == "__main__": + main()