Lab061: integrate T2 into the strict receiver
This commit is contained in:
@@ -2630,3 +2630,34 @@ Lab054. Штатный receiver и production-код не изменялись.
|
|||||||
метрики без расхождений. Lab060 не коммитировалась, push не выполнялся.
|
метрики без расхождений. Lab060 не коммитировалась, push не выполнялся.
|
||||||
Следующий этап: STOP и отдельно согласовать решение о внедрении T2 в штатный
|
Следующий этап: STOP и отдельно согласовать решение о внедрении T2 в штатный
|
||||||
receiver; Lab061 автоматически не создавать.
|
receiver; Lab061 автоматически не создавать.
|
||||||
|
|
||||||
|
# Запись 055
|
||||||
|
|
||||||
|
## Дата
|
||||||
|
|
||||||
|
11 сентября 2026 года
|
||||||
|
|
||||||
|
## Тема
|
||||||
|
|
||||||
|
Lab061 — OFFLINE-интеграция T2 в строгий приёмник
|
||||||
|
|
||||||
|
## Выполнено
|
||||||
|
|
||||||
|
Lab060 зафиксирована коммитом 92b0e0f8dd726f4420e8ef5d5e7c049357296e7c;
|
||||||
|
push не выполнялся. T2 встроен в существующий строгий PacketProcessor как явно
|
||||||
|
выбираемый режим. Legacy остался режимом по умолчанию, автоматической повторной
|
||||||
|
попытки по результату CRC нет. Коэффициенты и P3 численно совпали с Lab060.
|
||||||
|
|
||||||
|
Полный штатный synthetic PHY-пакет 500K проверен на39 случаях в каждом режиме:
|
||||||
|
13 остаточных частот от−10 до+10Гц и три качества. На всех качествах legacy/T2
|
||||||
|
дали CRC89,743590/94,871795%; на уровне EVM0,844032 — 9/13 и11/13. Сохранённый
|
||||||
|
TX дал16/16 в обоих режимах. Lab047 сохранил36/36, Lab054 сохранил0/16, фон
|
||||||
|
дал0 заголовков и0 пакетов.
|
||||||
|
|
||||||
|
Интегрированное ядро T2 воспроизвело Lab056: средняя SER A/B равна
|
||||||
|
0,017756/3,501780%, срыва сопровождения нет. Проверки причинности, отсутствия
|
||||||
|
payload truth и конечности при ошибках, fade и шуме прошли. Решение —
|
||||||
|
READY_BEHIND_EXPLICIT_MODE до отдельного контролируемого RF-теста с полными
|
||||||
|
500K-пакетами. Оборудование не опрашивалось, RF TX=0. Lab061 фиксируется этим
|
||||||
|
отдельным коммитом; push выполняется пользователем отдельно. Следующий этап:
|
||||||
|
STOP; Lab062 автоматически не начинать.
|
||||||
|
|||||||
BIN
data/processed/lab061/background_result.png
Normal file
BIN
data/processed/lab061/background_result.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
data/processed/lab061/lab047_regression.png
Normal file
BIN
data/processed/lab061/lab047_regression.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
data/processed/lab061/lab054_strict_result.png
Normal file
BIN
data/processed/lab061/lab054_strict_result.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
5
data/processed/lab061/lab056_results.csv
Normal file
5
data/processed/lab061/lab056_results.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
label,symbol_rate,symbol_errors,symbols,symbol_error_rate,evm_rms,matched_component_power,residual_power,matched_residual_db,final_frequency_hz,large_phase_slips,loss_of_lock,bad_blocks,longest_bad_state_seconds,self_recovered,stable_lock
|
||||||
|
A1,20000,1,2816,0.0003551136363636364,0.31627242306137465,0.9760338688912713,0.09763095554074837,9.998773482861347,-0.03678470540537988,0,False,0,0.0,False,True
|
||||||
|
B1,500000,3290,101120,0.032535601265822785,0.8090614605506008,0.940782269995664,0.615817678774771,1.840369716972757,-2.3940002814874086,0,False,0,0.0,False,True
|
||||||
|
A2,20000,0,2816,0.0,0.31568433619231595,1.012966734394142,0.10094882078152588,10.014939332951363,0.0013080327716195816,0,False,0,0.0,False,True
|
||||||
|
B2,500000,3792,101120,0.0375,0.8744054425662731,0.986059800896635,0.7539264125589239,1.1657429573714726,1.9343992902116445,0,False,0,0.0,False,True
|
||||||
|
BIN
data/processed/lab061/packet_success_by_initial_cfo.png
Normal file
BIN
data/processed/lab061/packet_success_by_initial_cfo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
105
data/processed/lab061/real_results.csv
Normal file
105
data/processed/lab061/real_results.csv
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
dataset,frame,mode,HEADER_ACCEPTED,FEC_STARTED,FEC_PASS,CRC_STARTED,CRC_PASS,PACKET_OUTPUT,reason,initial_frequency_hz
|
||||||
|
Lab047,1,legacy,True,True,True,True,True,True,CRC_PASS,0.2033868989670703
|
||||||
|
Lab047,2,legacy,True,True,True,True,True,True,CRC_PASS,0.012450970374705333
|
||||||
|
Lab047,3,legacy,True,True,True,True,True,True,CRC_PASS,-0.15241868588401689
|
||||||
|
Lab047,4,legacy,True,True,True,True,True,True,CRC_PASS,0.012628034912381616
|
||||||
|
Lab047,5,legacy,True,True,True,True,True,True,CRC_PASS,0.28102835180050534
|
||||||
|
Lab047,6,legacy,True,True,True,True,True,True,CRC_PASS,0.0728617397594959
|
||||||
|
Lab047,7,legacy,True,True,True,True,True,True,CRC_PASS,0.02153239849549078
|
||||||
|
Lab047,8,legacy,True,True,True,True,True,True,CRC_PASS,0.130332736445595
|
||||||
|
Lab047,9,legacy,True,True,True,True,True,True,CRC_PASS,0.08586075134986197
|
||||||
|
Lab047,10,legacy,True,True,True,True,True,True,CRC_PASS,0.019087882990447214
|
||||||
|
Lab047,11,legacy,True,True,True,True,True,True,CRC_PASS,0.012581360419676476
|
||||||
|
Lab047,12,legacy,True,True,True,True,True,True,CRC_PASS,0.18682072928588908
|
||||||
|
Lab047,13,legacy,True,True,True,True,True,True,CRC_PASS,0.10358464925085982
|
||||||
|
Lab047,14,legacy,True,True,True,True,True,True,CRC_PASS,-0.2112142577930461
|
||||||
|
Lab047,15,legacy,True,True,True,True,True,True,CRC_PASS,0.04987013681457267
|
||||||
|
Lab047,16,legacy,True,True,True,True,True,True,CRC_PASS,-0.018063387420586025
|
||||||
|
Lab047,17,legacy,True,True,True,True,True,True,CRC_PASS,-0.10920375479998666
|
||||||
|
Lab047,18,legacy,True,True,True,True,True,True,CRC_PASS,-0.023768436535983758
|
||||||
|
Lab047,19,legacy,True,True,True,True,True,True,CRC_PASS,-0.12844207213561043
|
||||||
|
Lab047,20,legacy,True,True,True,True,True,True,CRC_PASS,-0.021131644411390132
|
||||||
|
Lab047,21,legacy,True,True,True,True,True,True,CRC_PASS,0.0010757873345088082
|
||||||
|
Lab047,22,legacy,True,True,True,True,True,True,CRC_PASS,0.16827983008186687
|
||||||
|
Lab047,23,legacy,True,True,True,True,True,True,CRC_PASS,0.030806194252629853
|
||||||
|
Lab047,24,legacy,True,True,True,True,True,True,CRC_PASS,0.03249681818606507
|
||||||
|
Lab047,25,legacy,True,True,True,True,True,True,CRC_PASS,-0.1189326326882962
|
||||||
|
Lab047,26,legacy,True,True,True,True,True,True,CRC_PASS,-0.08475150493996597
|
||||||
|
Lab047,27,legacy,True,True,True,True,True,True,CRC_PASS,0.05389239025811241
|
||||||
|
Lab047,28,legacy,True,True,True,True,True,True,CRC_PASS,0.04289420557710872
|
||||||
|
Lab047,29,legacy,True,True,True,True,True,True,CRC_PASS,-0.23616554750888527
|
||||||
|
Lab047,30,legacy,True,True,True,True,True,True,CRC_PASS,-0.12603744749648454
|
||||||
|
Lab047,31,legacy,True,True,True,True,True,True,CRC_PASS,0.0605922709725388
|
||||||
|
Lab047,32,legacy,True,True,True,True,True,True,CRC_PASS,-0.08349857262528622
|
||||||
|
Lab047,33,legacy,True,True,True,True,True,True,CRC_PASS,-0.15898835195874458
|
||||||
|
Lab047,34,legacy,True,True,True,True,True,True,CRC_PASS,0.1138327859367743
|
||||||
|
Lab047,35,legacy,True,True,True,True,True,True,CRC_PASS,-0.23280446158763507
|
||||||
|
Lab047,36,legacy,True,True,True,True,True,True,CRC_PASS,-0.2555997057508273
|
||||||
|
Lab047,1,t2,True,True,True,True,True,True,CRC_PASS,0.15714547272293758
|
||||||
|
Lab047,2,t2,True,True,True,True,True,True,CRC_PASS,0.04525547988229565
|
||||||
|
Lab047,3,t2,True,True,True,True,True,True,CRC_PASS,-0.012490675017939984
|
||||||
|
Lab047,4,t2,True,True,True,True,True,True,CRC_PASS,0.08847136354602585
|
||||||
|
Lab047,5,t2,True,True,True,True,True,True,CRC_PASS,0.1939244013504176
|
||||||
|
Lab047,6,t2,True,True,True,True,True,True,CRC_PASS,0.055162738246807015
|
||||||
|
Lab047,7,t2,True,True,True,True,True,True,CRC_PASS,0.04068198959955451
|
||||||
|
Lab047,8,t2,True,True,True,True,True,True,CRC_PASS,0.03634649209435207
|
||||||
|
Lab047,9,t2,True,True,True,True,True,True,CRC_PASS,-0.03437239082431202
|
||||||
|
Lab047,10,t2,True,True,True,True,True,True,CRC_PASS,-0.004015083453945893
|
||||||
|
Lab047,11,t2,True,True,True,True,True,True,CRC_PASS,0.08945444080931844
|
||||||
|
Lab047,12,t2,True,True,True,True,True,True,CRC_PASS,0.20793181274469477
|
||||||
|
Lab047,13,t2,True,True,True,True,True,True,CRC_PASS,0.07046137666433454
|
||||||
|
Lab047,14,t2,True,True,True,True,True,True,CRC_PASS,-0.07105944506834289
|
||||||
|
Lab047,15,t2,True,True,True,True,True,True,CRC_PASS,0.0609663723289962
|
||||||
|
Lab047,16,t2,True,True,True,True,True,True,CRC_PASS,-0.07015498366163844
|
||||||
|
Lab047,17,t2,True,True,True,True,True,True,CRC_PASS,-0.11928091623072502
|
||||||
|
Lab047,18,t2,True,True,True,True,True,True,CRC_PASS,0.04019810967068637
|
||||||
|
Lab047,19,t2,True,True,True,True,True,True,CRC_PASS,0.015930924737407013
|
||||||
|
Lab047,20,t2,True,True,True,True,True,True,CRC_PASS,-0.008585663400001449
|
||||||
|
Lab047,21,t2,True,True,True,True,True,True,CRC_PASS,0.03878383029001333
|
||||||
|
Lab047,22,t2,True,True,True,True,True,True,CRC_PASS,0.09427373288664782
|
||||||
|
Lab047,23,t2,True,True,True,True,True,True,CRC_PASS,0.0003722803565952149
|
||||||
|
Lab047,24,t2,True,True,True,True,True,True,CRC_PASS,-0.008086735812339499
|
||||||
|
Lab047,25,t2,True,True,True,True,True,True,CRC_PASS,0.02904818250697064
|
||||||
|
Lab047,26,t2,True,True,True,True,True,True,CRC_PASS,-0.00973707735031404
|
||||||
|
Lab047,27,t2,True,True,True,True,True,True,CRC_PASS,0.05145139978648825
|
||||||
|
Lab047,28,t2,True,True,True,True,True,True,CRC_PASS,-0.07130299348886393
|
||||||
|
Lab047,29,t2,True,True,True,True,True,True,CRC_PASS,-0.04495129868231881
|
||||||
|
Lab047,30,t2,True,True,True,True,True,True,CRC_PASS,-0.11170196243926128
|
||||||
|
Lab047,31,t2,True,True,True,True,True,True,CRC_PASS,-0.06762881454676493
|
||||||
|
Lab047,32,t2,True,True,True,True,True,True,CRC_PASS,-0.05377742451538007
|
||||||
|
Lab047,33,t2,True,True,True,True,True,True,CRC_PASS,-0.05983507118706565
|
||||||
|
Lab047,34,t2,True,True,True,True,True,True,CRC_PASS,-0.03974346979852063
|
||||||
|
Lab047,35,t2,True,True,True,True,True,True,CRC_PASS,-0.04857303052983457
|
||||||
|
Lab047,36,t2,True,True,True,True,True,True,CRC_PASS,-0.0800518478779996
|
||||||
|
Lab054,1,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,2,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,3,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,4,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,5,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,6,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,7,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,8,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,9,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,10,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,11,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,12,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,13,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,14,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,15,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,16,legacy,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,1,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,2,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,3,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,4,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,5,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,6,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,7,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,8,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,9,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,10,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,11,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,12,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,13,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,14,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,15,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
Lab054,16,t2,False,False,False,False,False,False,PHY_HEADER_MAGIC_MISMATCH,
|
||||||
|
6
data/processed/lab061/robustness_results.csv
Normal file
6
data/processed/lab061/robustness_results.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
case,finite,max_abs_frequency_hz
|
||||||
|
single,True,0.0021597810674307996
|
||||||
|
short,True,0.0021597810674307996
|
||||||
|
long,True,0.0021597810674307996
|
||||||
|
amplitude_drop,True,0.0021597810674307996
|
||||||
|
noise,True,1.6764279120281116
|
||||||
|
184
data/processed/lab061/summary.json
Normal file
184
data/processed/lab061/summary.json
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"lab": "Lab061",
|
||||||
|
"status": "COMPLETED",
|
||||||
|
"scope": "OFFLINE",
|
||||||
|
"rf_tx": 0,
|
||||||
|
"hardware_queries": 0,
|
||||||
|
"legacy_default": true,
|
||||||
|
"automatic_fallback": false,
|
||||||
|
"t2_parameters": {
|
||||||
|
"normalized_bandwidth": 0.0005,
|
||||||
|
"damping_factor": 0.7071067811865475,
|
||||||
|
"phase_coefficient": 0.0013324447407406753,
|
||||||
|
"frequency_coefficient": 8.882964938271169e-07,
|
||||||
|
"detector_clip_rad": 1.0471975511965976
|
||||||
|
},
|
||||||
|
"synthetic": {
|
||||||
|
"legacy": {
|
||||||
|
"header_pass_rate": 0.9230769230769231,
|
||||||
|
"crc_pass_rate": 0.8974358974358975
|
||||||
|
},
|
||||||
|
"t2": {
|
||||||
|
"header_pass_rate": 0.9487179487179487,
|
||||||
|
"crc_pass_rate": 0.9487179487179487
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"t2_packet_success_by_initial_frequency_error": {
|
||||||
|
"-10": 0.6666666666666666,
|
||||||
|
"-5": 1.0,
|
||||||
|
"-2": 1.0,
|
||||||
|
"-1": 1.0,
|
||||||
|
"-0.5": 1.0,
|
||||||
|
"-0.25": 1.0,
|
||||||
|
"0": 1.0,
|
||||||
|
"0.25": 1.0,
|
||||||
|
"0.5": 1.0,
|
||||||
|
"1": 1.0,
|
||||||
|
"2": 1.0,
|
||||||
|
"5": 0.6666666666666666,
|
||||||
|
"10": 1.0
|
||||||
|
},
|
||||||
|
"real": {
|
||||||
|
"Lab047": {
|
||||||
|
"legacy": 36,
|
||||||
|
"t2": 36
|
||||||
|
},
|
||||||
|
"Lab054": {
|
||||||
|
"legacy": 0,
|
||||||
|
"t2": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lab056": [
|
||||||
|
{
|
||||||
|
"label": "A1",
|
||||||
|
"symbol_rate": 20000,
|
||||||
|
"symbol_errors": 1,
|
||||||
|
"symbols": 2816,
|
||||||
|
"symbol_error_rate": 0.0003551136363636364,
|
||||||
|
"evm_rms": 0.31627242306137465,
|
||||||
|
"matched_component_power": 0.9760338688912713,
|
||||||
|
"residual_power": 0.09763095554074837,
|
||||||
|
"matched_residual_db": 9.998773482861347,
|
||||||
|
"final_frequency_hz": -0.03678470540537988,
|
||||||
|
"large_phase_slips": 0,
|
||||||
|
"loss_of_lock": false,
|
||||||
|
"bad_blocks": 0,
|
||||||
|
"longest_bad_state_seconds": 0.0,
|
||||||
|
"self_recovered": false,
|
||||||
|
"stable_lock": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "B1",
|
||||||
|
"symbol_rate": 500000,
|
||||||
|
"symbol_errors": 3290,
|
||||||
|
"symbols": 101120,
|
||||||
|
"symbol_error_rate": 0.032535601265822785,
|
||||||
|
"evm_rms": 0.8090614605506008,
|
||||||
|
"matched_component_power": 0.940782269995664,
|
||||||
|
"residual_power": 0.615817678774771,
|
||||||
|
"matched_residual_db": 1.840369716972757,
|
||||||
|
"final_frequency_hz": -2.3940002814874086,
|
||||||
|
"large_phase_slips": 0,
|
||||||
|
"loss_of_lock": false,
|
||||||
|
"bad_blocks": 0,
|
||||||
|
"longest_bad_state_seconds": 0.0,
|
||||||
|
"self_recovered": false,
|
||||||
|
"stable_lock": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "A2",
|
||||||
|
"symbol_rate": 20000,
|
||||||
|
"symbol_errors": 0,
|
||||||
|
"symbols": 2816,
|
||||||
|
"symbol_error_rate": 0.0,
|
||||||
|
"evm_rms": 0.31568433619231595,
|
||||||
|
"matched_component_power": 1.012966734394142,
|
||||||
|
"residual_power": 0.10094882078152588,
|
||||||
|
"matched_residual_db": 10.014939332951363,
|
||||||
|
"final_frequency_hz": 0.0013080327716195816,
|
||||||
|
"large_phase_slips": 0,
|
||||||
|
"loss_of_lock": false,
|
||||||
|
"bad_blocks": 0,
|
||||||
|
"longest_bad_state_seconds": 0.0,
|
||||||
|
"self_recovered": false,
|
||||||
|
"stable_lock": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "B2",
|
||||||
|
"symbol_rate": 500000,
|
||||||
|
"symbol_errors": 3792,
|
||||||
|
"symbols": 101120,
|
||||||
|
"symbol_error_rate": 0.0375,
|
||||||
|
"evm_rms": 0.8744054425662731,
|
||||||
|
"matched_component_power": 0.986059800896635,
|
||||||
|
"residual_power": 0.7539264125589239,
|
||||||
|
"matched_residual_db": 1.1657429573714726,
|
||||||
|
"final_frequency_hz": 1.9343992902116445,
|
||||||
|
"large_phase_slips": 0,
|
||||||
|
"loss_of_lock": false,
|
||||||
|
"bad_blocks": 0,
|
||||||
|
"longest_bad_state_seconds": 0.0,
|
||||||
|
"self_recovered": false,
|
||||||
|
"stable_lock": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"legacy": {
|
||||||
|
"headers": 0,
|
||||||
|
"packets": 0,
|
||||||
|
"candidates": 13
|
||||||
|
},
|
||||||
|
"t2": {
|
||||||
|
"headers": 0,
|
||||||
|
"packets": 0,
|
||||||
|
"candidates": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saved_tx_control": {
|
||||||
|
"legacy": {
|
||||||
|
"headers": 16,
|
||||||
|
"packets": 16
|
||||||
|
},
|
||||||
|
"t2": {
|
||||||
|
"headers": 16,
|
||||||
|
"packets": 16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"robustness": [
|
||||||
|
{
|
||||||
|
"case": "single",
|
||||||
|
"finite": true,
|
||||||
|
"max_abs_frequency_hz": 0.0021597810674307996
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "short",
|
||||||
|
"finite": true,
|
||||||
|
"max_abs_frequency_hz": 0.0021597810674307996
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "long",
|
||||||
|
"finite": true,
|
||||||
|
"max_abs_frequency_hz": 0.0021597810674307996
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "amplitude_drop",
|
||||||
|
"finite": true,
|
||||||
|
"max_abs_frequency_hz": 0.0021597810674307996
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "noise",
|
||||||
|
"finite": true,
|
||||||
|
"max_abs_frequency_hz": 1.6764279120281116
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload_truth_leak_test": "PASS: strict API and T2 have no reference payload input; causal prefix test PASS",
|
||||||
|
"production_decision": "READY_BEHIND_EXPLICIT_MODE",
|
||||||
|
"physical_packet": {
|
||||||
|
"bytes": 268,
|
||||||
|
"symbols": 5724,
|
||||||
|
"duration_seconds": 0.011532
|
||||||
|
},
|
||||||
|
"source_sha256": {
|
||||||
|
"data\\raw\\lab054\\rx_stream.cs8": "641886879964250c315dd399d469503808aae43fb319bef05e0bf2a78bf14585"
|
||||||
|
}
|
||||||
|
}
|
||||||
79
data/processed/lab061/synthetic_packet_results.csv
Normal file
79
data/processed/lab061/synthetic_packet_results.csv
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
quality,noise_sigma,initial_frequency_error_hz,initial_phase_rad,seed,mode,measured_evm_rms,candidates,HEADER_ACCEPTED,FEC_STARTED,FEC_PASS,CRC_STARTED,CRC_PASS,PACKET_OUTPUT
|
||||||
|
GOOD,2.0,-10,0.37,61000,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-10,0.37,61000,t2,0.01982606424337177,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-5,0.37,61001,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-5,0.37,61001,t2,0.019746181325301454,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-2,0.37,61002,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-2,0.37,61002,t2,0.019842442865977007,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-1,0.37,61003,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-1,0.37,61003,t2,0.019853572202090563,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-0.5,0.37,61004,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-0.5,0.37,61004,t2,0.01981070684458435,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-0.25,0.37,61005,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,-0.25,0.37,61005,t2,0.01993850807040547,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,0,0.37,61006,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,0,0.37,61006,t2,0.019706768331930264,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,0.25,0.37,61007,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,0.25,0.37,61007,t2,0.019694517437587915,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,0.5,0.37,61008,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,0.5,0.37,61008,t2,0.01985943013101642,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,1,0.37,61009,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,1,0.37,61009,t2,0.01973023527181751,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,2,0.37,61010,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,2,0.37,61010,t2,0.019705618251859375,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,5,0.37,61011,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,5,0.37,61011,t2,0.01945222847829509,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,10,0.37,61012,legacy,,1,True,True,True,True,True,True
|
||||||
|
GOOD,2.0,10,0.37,61012,t2,0.01990311396663494,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-10,0.37,61100,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-10,0.37,61100,t2,0.19171434819251312,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-5,0.37,61101,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-5,0.37,61101,t2,0.19397897593420543,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-2,0.37,61102,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-2,0.37,61102,t2,0.19306787955568316,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-1,0.37,61103,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-1,0.37,61103,t2,0.19099888656356456,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-0.5,0.37,61104,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-0.5,0.37,61104,t2,0.19250262062488108,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-0.25,0.37,61105,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,-0.25,0.37,61105,t2,0.19221599734407988,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,0,0.37,61106,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,0,0.37,61106,t2,0.19404155054144742,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,0.25,0.37,61107,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,0.25,0.37,61107,t2,0.19412644854555466,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,0.5,0.37,61108,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,0.5,0.37,61108,t2,0.18965910158764945,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,1,0.37,61109,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,1,0.37,61109,t2,0.1948138602914489,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,2,0.37,61110,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,2,0.37,61110,t2,0.19457771777218613,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,5,0.37,61111,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,5,0.37,61111,t2,0.19327098549387706,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,10,0.37,61112,legacy,,1,True,True,True,True,True,True
|
||||||
|
MEDIUM,24.0,10,0.37,61112,t2,0.19404771262916432,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-10,0.37,61200,legacy,,10,False,False,False,False,False,False
|
||||||
|
LAB056_B_LIKE,105.0,-10,0.37,61200,t2,,10,False,False,False,False,False,False
|
||||||
|
LAB056_B_LIKE,105.0,-5,0.37,61201,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-5,0.37,61201,t2,0.8603198486837162,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-2,0.37,61202,legacy,,14,True,True,True,True,False,False
|
||||||
|
LAB056_B_LIKE,105.0,-2,0.37,61202,t2,0.8346182114574645,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-1,0.37,61203,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-1,0.37,61203,t2,0.8258615362415264,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-0.5,0.37,61204,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-0.5,0.37,61204,t2,0.8399635622236176,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-0.25,0.37,61205,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,-0.25,0.37,61205,t2,0.850324698425276,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,0,0.37,61206,legacy,,14,False,False,False,False,False,False
|
||||||
|
LAB056_B_LIKE,105.0,0,0.37,61206,t2,0.8514425010406356,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,0.25,0.37,61207,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,0.25,0.37,61207,t2,0.859691631568735,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,0.5,0.37,61208,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,0.5,0.37,61208,t2,0.8454644268223601,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,1,0.37,61209,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,1,0.37,61209,t2,0.8408531075244954,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,2,0.37,61210,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,2,0.37,61210,t2,0.8303200529343647,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,5,0.37,61211,legacy,,23,False,False,False,False,False,False
|
||||||
|
LAB056_B_LIKE,105.0,5,0.37,61211,t2,,23,False,False,False,False,False,False
|
||||||
|
LAB056_B_LIKE,105.0,10,0.37,61212,legacy,,1,True,True,True,True,True,True
|
||||||
|
LAB056_B_LIKE,105.0,10,0.37,61212,t2,0.8454912251538359,1,True,True,True,True,True,True
|
||||||
|
BIN
data/processed/lab061/synthetic_t2_frequency.png
Normal file
BIN
data/processed/lab061/synthetic_t2_frequency.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
data/processed/lab061/synthetic_t2_phase.png
Normal file
BIN
data/processed/lab061/synthetic_t2_phase.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
data/processed/lab061/trajectories.npz
Normal file
BIN
data/processed/lab061/trajectories.npz
Normal file
Binary file not shown.
@@ -547,6 +547,26 @@
|
|||||||
[JSON](../data/processed/lab060/summary.json),
|
[JSON](../data/processed/lab060/summary.json),
|
||||||
[real CSV](../data/processed/lab060/real_results.csv),
|
[real CSV](../data/processed/lab060/real_results.csv),
|
||||||
[synthetic CSV](../data/processed/lab060/synthetic_results.csv).
|
[synthetic CSV](../data/processed/lab060/synthetic_results.csv).
|
||||||
- **STOP:** исходные IQ не изменились, Lab060 не коммитировалась, push не
|
- **Фиксация:** Lab060 зафиксирована коммитом
|
||||||
выполнялся, Lab061 не создана. Следующий этап — отдельно согласовать решение
|
`92b0e0f8dd726f4420e8ef5d5e7c049357296e7c`; push не выполнялся.
|
||||||
о внедрении T2 в штатный receiver.
|
|
||||||
|
## Lab061 — T2 в строгом приёмнике
|
||||||
|
|
||||||
|
- **Запись 055; только offline, RF TX=0.** T2 встроен в существующий
|
||||||
|
`PacketProcessor` как явный режим; legacy остался значением по умолчанию.
|
||||||
|
Автоматического CRC-fallback нет, строгие header/FEC/CRC правила не менялись.
|
||||||
|
- **Полный synthetic PHY 500K:** 39 случаев на режим, CFO от −10 до +10 Гц,
|
||||||
|
три качества. Header legacy/T2 92,307692/94,871795%, CRC
|
||||||
|
89,743590/94,871795%. На EVM0,844032 packets 9/13 против11/13.
|
||||||
|
- **Реальные контроли:** Lab047 legacy/T2 36/36; Lab054 0/16; saved TX 16/16;
|
||||||
|
background 0 headers и0 packets. Исторический Lab054 `NOT_A` сохранён.
|
||||||
|
- **Lab056:** средняя SER A/B 0,017756/3,501780%, потери lock нет. Реальная
|
||||||
|
широкополосная ошибка около3,5% остаётся физическим ограничением.
|
||||||
|
- **Решение:** `READY_BEHIND_EXPLICIT_MODE` до контролируемого RF-теста с
|
||||||
|
полными 500K-пакетами. [План](lab061_current_state.md),
|
||||||
|
[отчёт](lab061_offline_result.md),
|
||||||
|
[JSON](../data/processed/lab061/summary.json),
|
||||||
|
[real CSV](../data/processed/lab061/real_results.csv),
|
||||||
|
[synthetic CSV](../data/processed/lab061/synthetic_packet_results.csv).
|
||||||
|
- **STOP:** Lab061 фиксируется этим отдельным коммитом; push выполняется
|
||||||
|
пользователем отдельно, Lab062 не создана.
|
||||||
|
|||||||
52
docs/lab061_current_state.md
Normal file
52
docs/lab061_current_state.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Lab061 — план интеграции T2 в строгий приёмник
|
||||||
|
|
||||||
|
Lab061 выполняется только офлайн. `RF TX = 0`; HackRF не опрашивается.
|
||||||
|
|
||||||
|
## Фактический путь приёмника
|
||||||
|
|
||||||
|
`lab053_streaming_receiver.StreamingReceiver` выполняет независимый поиск
|
||||||
|
кандидатов и передаёт заранее выбранному `PacketProcessor` IQ-окно. Внутри
|
||||||
|
`PacketProcessor` последовательно работают RRC-фильтр, временная
|
||||||
|
синхронизация, оценка несущей, извлечение мягких символов, строгий разбор PHY
|
||||||
|
header, Viterbi FEC, parser и CRC.
|
||||||
|
|
||||||
|
T2 добавляется в этот существующий путь после RRC и timing. Начальное состояние
|
||||||
|
P3 вычисляется по квадрату всей 1280-символьной преамбулы. После преамбулы T2
|
||||||
|
видит только текущий символ, собственное BPSK-решение и предыдущее состояние.
|
||||||
|
Header, payload, CRC и будущие символы ему недоступны.
|
||||||
|
|
||||||
|
Среднее DC в T2 считается только до последнего raw-отсчёта, который через
|
||||||
|
причинную импульсную характеристику RRC способен повлиять на последнюю точку
|
||||||
|
P3. Отсчёты header и payload не входят и в эту подготовительную оценку.
|
||||||
|
|
||||||
|
Режим задаётся до запуска как `carrier_tracking_mode="legacy"` или `"t2"`.
|
||||||
|
Значение по умолчанию — `legacy`. Повторной попытки после CRC FAIL нет.
|
||||||
|
|
||||||
|
## Зафиксированная математика
|
||||||
|
|
||||||
|
При нормированной полосе `Bn=0.0005` и затухании `zeta=1/sqrt(2)`:
|
||||||
|
|
||||||
|
`theta = Bn / (zeta + 1/(4*zeta))`
|
||||||
|
|
||||||
|
`alpha = 4*zeta*theta / (1 + 2*zeta*theta + theta^2)`
|
||||||
|
|
||||||
|
`beta = 4*theta^2 / (1 + 2*zeta*theta + theta^2)`
|
||||||
|
|
||||||
|
Получаются `alpha=0.0013324447407406753` и
|
||||||
|
`beta=8.882964938271169e-7` на один символ. Частотное состояние внутри петли
|
||||||
|
хранится в радианах на символ; для отчёта оно переводится в герцы как
|
||||||
|
`omega*Rs/(2*pi)`. Коэффициенты Lab060 не перенастраиваются.
|
||||||
|
|
||||||
|
## Критерии
|
||||||
|
|
||||||
|
- чистый полный штатный PHY-пакет 500 ksymbol/s проходит поиск, header, FEC и CRC;
|
||||||
|
- сравнение legacy/T2 выполняется при заранее выбранном режиме;
|
||||||
|
- Lab047 остаётся 36/36 в обоих режимах;
|
||||||
|
- Lab054 остаётся историческим `NOT_A, 0/16`, если строгий путь его не восстановит;
|
||||||
|
- background даёт ноль header и packet;
|
||||||
|
- изменение диагностической reference payload не меняет решения или траекторию T2;
|
||||||
|
- алгоритм остаётся конечным при ошибках символов, провале амплитуды и шуме.
|
||||||
|
|
||||||
|
После завершения и независимой проверки Lab061 фиксируется отдельным коммитом
|
||||||
|
по разрешению пользователя. Push выполняется пользователем отдельно. Lab062 не
|
||||||
|
создаётся автоматически.
|
||||||
92
docs/lab061_offline_result.md
Normal file
92
docs/lab061_offline_result.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Lab061 — T2 в строгом приёмнике
|
||||||
|
|
||||||
|
## Результат простыми словами
|
||||||
|
|
||||||
|
T2 перенесён из диагностической Lab060 в тот же строгий путь, который ищет
|
||||||
|
пакет, синхронизируется, разбирает PHY header, запускает FEC и принимает пакет
|
||||||
|
только после CRC PASS. Режим включается явно; обычный запуск по-прежнему
|
||||||
|
использует legacy. Скрытого повторного декодирования нет.
|
||||||
|
|
||||||
|
На сохранённом Lab047 оба режима приняли 36 из 36 пакетов. На Lab054 оба дали
|
||||||
|
0 из 16: исторический результат `NOT_A` не изменён. Чистая сохранённая TX-форма
|
||||||
|
дала 16/16 в обоих режимах, фон — ноль допустимых заголовков и пакетов.
|
||||||
|
|
||||||
|
## Полный синтетический PHY-пакет 500K
|
||||||
|
|
||||||
|
Штатный генератор сформировал преамбулу, повторённый header, FEC-кодированное
|
||||||
|
тело и CRC. Проверено 39 реализаций для каждого режима: 13 начальных ошибок
|
||||||
|
частоты от −10 до +10 Гц, фиксированная начальная фаза и три уровня шума.
|
||||||
|
Условное среднее измеренное EVM уровня `LAB056_B_LIKE` равно 0,844032 по 11
|
||||||
|
полным T2-траекториям из 13; два случая, остановившиеся раньше полного
|
||||||
|
извлечения, в это среднее не входят.
|
||||||
|
|
||||||
|
| Режим | Header | CRC и packet |
|
||||||
|
|---|---:|---:|
|
||||||
|
| legacy | 92,307692% | 89,743590% |
|
||||||
|
| T2 | 94,871795% | 94,871795% |
|
||||||
|
|
||||||
|
На качестве около Lab056 B legacy принял 9/13 пакетов, T2 — 11/13. На двух
|
||||||
|
лучших уровнях оба режима приняли 26/26. По всем трём уровням доля T2 для CFO
|
||||||
|
−10/−5/−2/−1/−0,5/−0,25/0/+0,25/+0,5/+1/+2/+5/+10 Гц составила
|
||||||
|
66,67/100/100/100/100/100/100/100/100/100/100/66,67/100%.
|
||||||
|
|
||||||
|
Текущий штатный пакет с 256 байтами payload содержит 268 packet bytes и 5724
|
||||||
|
символа; его сформированная длительность — 11,532 мс. Указанные ранее 204,8 мс
|
||||||
|
относятся к длинному диагностическому B-burst Lab056 из 102400 символов, а не к
|
||||||
|
этому штатному пакету. На всём B-burst T2 также проверен: срыва сопровождения
|
||||||
|
нет.
|
||||||
|
|
||||||
|
## Сохранённые данные
|
||||||
|
|
||||||
|
Средняя ошибка символов Lab056 после интегрированного ядра T2:
|
||||||
|
|
||||||
|
- A: `(1 + 0) / (2816 + 2816) = 0,017756%`;
|
||||||
|
- B: `(3290 + 3792) / (101120 + 101120) = 3,501780%`.
|
||||||
|
|
||||||
|
B1/B2 дали 3,253560% и 3,750000%; A1/A2 — 0,035511% и 0%. Все четыре
|
||||||
|
траектории сохранили устойчивый внутренний lock без больших фазовых скачков.
|
||||||
|
|
||||||
|
Для 36 пакетов Lab047 начальная P3-оценка лежала от −0,119281 до +0,207932 Гц.
|
||||||
|
Это далеко внутри проверенного диапазона; прежнее ограничение 20 ksymbol/s при
|
||||||
|
искусственной ошибке до ±10 Гц сохраняется. Отдельная настройка 20K не вводилась.
|
||||||
|
|
||||||
|
Lab054 дал в обоих режимах 0 header, 0 FEC, 0 CRC и 0 packet из 16 проверенных
|
||||||
|
исторических кандидатов. T2 не переопределяет этот отрицательный результат.
|
||||||
|
|
||||||
|
## Независимость и отказоустойчивость
|
||||||
|
|
||||||
|
T2 не принимает reference payload. Тест причинности меняет последний будущий
|
||||||
|
символ и подтверждает полное равенство всех предыдущих решений и состояний.
|
||||||
|
Отдельный тест меняет все raw-отсчёты после причинной границы P3 и подтверждает
|
||||||
|
равенство начальных частоты и комплексного коэффициента P3 с допуском `1e-8`;
|
||||||
|
остаток порядка `4e-9 Гц` возникает из-за округления FFT-свёртки.
|
||||||
|
Изменение диагностической reference меняет только последующее сравнение; путь
|
||||||
|
приёма и его RX bits от неё не зависят.
|
||||||
|
|
||||||
|
Одиночная ошибка, серии из 10 и 80 ошибок, провал амплитуды и шумовой участок
|
||||||
|
не создали NaN/Inf. Максимальное наблюдаемое частотное состояние составило
|
||||||
|
1,676428 Гц; дополнительный state clamp не вводился. На реальном фоне оба
|
||||||
|
режима проверили 13 кандидатов и не приняли ни одного заголовка или пакета.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
`PRODUCTION DECISION = READY_BEHIND_EXPLICIT_MODE`.
|
||||||
|
|
||||||
|
T2 численно совпадает с Lab060, полезен на полном строгом пути и не создаёт
|
||||||
|
регрессии Lab047, saved TX или background. Его рано делать режимом по умолчанию:
|
||||||
|
Lab054 остаётся 0/16, а контролируемого RF-сеанса с полноценными 500K-пакетами и
|
||||||
|
заранее выбранным T2 ещё не было.
|
||||||
|
|
||||||
|
После исправления программного сопровождения реальный широкий B-сигнал Lab056
|
||||||
|
всё равно имеет около 3,5% ошибочных символов при хорошем диагностическом
|
||||||
|
выравнивании. Следующая физическая задача — проверить энергетику и качество
|
||||||
|
широкополосного канала на полноценном пакете в одном заранее разрешённом
|
||||||
|
контролируемом опыте. Это отдельная лаборатория и здесь не выполнялось.
|
||||||
|
|
||||||
|
[Сводка JSON](../data/processed/lab061/summary.json),
|
||||||
|
[реальные результаты](../data/processed/lab061/real_results.csv),
|
||||||
|
[синтетические пакеты](../data/processed/lab061/synthetic_packet_results.csv),
|
||||||
|
[траектории](../data/processed/lab061/trajectories.npz).
|
||||||
|
|
||||||
|
`RF TX = 0`. Lab061 фиксируется этим отдельным коммитом. Push выполняется
|
||||||
|
пользователем отдельно. Lab062 не создавалась.
|
||||||
@@ -25,6 +25,7 @@ from scipy.signal import oaconvolve
|
|||||||
|
|
||||||
from experiments import lab052_candidate_detection as detection
|
from experiments import lab052_candidate_detection as detection
|
||||||
from experiments.lab053_viterbi import viterbi_decode_soft, warmup
|
from experiments.lab053_viterbi import viterbi_decode_soft, warmup
|
||||||
|
from protocol import bpsk_carrier_tracking as carrier_tracking
|
||||||
|
|
||||||
previous = detection.previous
|
previous = detection.previous
|
||||||
physical = previous.lab045
|
physical = previous.lab045
|
||||||
@@ -83,9 +84,11 @@ def measured(name):
|
|||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=16)
|
@lru_cache(maxsize=16)
|
||||||
def rotation(length: int) -> np.ndarray:
|
def rotation(length: int, sample_rate_hz: int = SAMPLE_RATE) -> np.ndarray:
|
||||||
indexes = np.arange(length)
|
indexes = np.arange(length)
|
||||||
values = np.exp(-2j * np.pi * lab043.CARRIER_OFFSET_HZ * indexes / SAMPLE_RATE).astype(np.complex64)
|
values = np.exp(
|
||||||
|
-2j * np.pi * lab043.CARRIER_OFFSET_HZ * indexes / sample_rate_hz
|
||||||
|
).astype(np.complex64)
|
||||||
values.flags.writeable = False
|
values.flags.writeable = False
|
||||||
return values
|
return values
|
||||||
|
|
||||||
@@ -97,24 +100,46 @@ class PacketProcessor:
|
|||||||
Полное тело по-прежнему запрашивается только после валидного PHY header.
|
Полное тело по-прежнему запрашивается только после валидного PHY header.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, viterbi=viterbi_decode_soft):
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
viterbi=viterbi_decode_soft,
|
||||||
|
carrier_tracking_mode: str = "legacy",
|
||||||
|
sample_rate_hz: int = SAMPLE_RATE,
|
||||||
|
symbol_rate: int = SYMBOL_RATE,
|
||||||
|
):
|
||||||
|
if carrier_tracking_mode not in ("legacy", "t2"):
|
||||||
|
raise ValueError("carrier_tracking_mode должен быть legacy или t2")
|
||||||
|
if type(sample_rate_hz) is not int or type(symbol_rate) is not int:
|
||||||
|
raise TypeError("Fs и Rs должны быть целыми")
|
||||||
|
if sample_rate_hz <= 0 or symbol_rate <= 0 or sample_rate_hz % symbol_rate:
|
||||||
|
raise ValueError("Fs должен быть положительным целым кратным Rs")
|
||||||
|
self.carrier_tracking_mode = carrier_tracking_mode
|
||||||
|
self.sample_rate_hz = sample_rate_hz
|
||||||
|
self.symbol_rate = symbol_rate
|
||||||
|
self.samples_per_symbol = sample_rate_hz // symbol_rate
|
||||||
self.startup = warmup() if viterbi is viterbi_decode_soft else {"backend": "CUSTOM", "warmup_seconds": 0}
|
self.startup = warmup() if viterbi is viterbi_decode_soft else {"backend": "CUSTOM", "warmup_seconds": 0}
|
||||||
constants = {"SAMPLES_PER_SYMBOL": SAMPLES_PER_SYMBOL, "SYMBOL_RATE": SYMBOL_RATE}
|
constants = {
|
||||||
|
"SAMPLES_PER_SYMBOL": self.samples_per_symbol,
|
||||||
|
"SYMBOL_RATE": self.symbol_rate,
|
||||||
|
}
|
||||||
scale = bind(physical.lab044._scale_and_carrier, **constants)
|
scale = bind(physical.lab044._scale_and_carrier, **constants)
|
||||||
self.bits = physical.lab044.preamble_bits().copy()
|
self.bits = physical.lab044.preamble_bits().copy()
|
||||||
waveform = scale(previous.iq.radio.bpsk_modulate(self.bits))
|
waveform = scale(previous.iq.radio.bpsk_modulate(self.bits))
|
||||||
self.reference = physical.lab044.signed_iq_bytes_to_complex64(lab043.to_cs8_bytes(waveform))
|
self.reference = physical.lab044.signed_iq_bytes_to_complex64(lab043.to_cs8_bytes(waveform))
|
||||||
self.taps = bind(lab043._matched_filter_taps, **constants)()
|
self.taps = bind(lab043._matched_filter_taps, **constants)()
|
||||||
self.expected = previous.iq.radio.bpsk_modulate(self.bits)
|
self.expected = previous.iq.radio.bpsk_modulate(self.bits)
|
||||||
self.kernel = np.zeros((len(self.bits) - 1) * SAMPLES_PER_SYMBOL + 1, np.complex64)
|
self.kernel = np.zeros(
|
||||||
self.kernel[::SAMPLES_PER_SYMBOL] = np.conj(self.expected)
|
(len(self.bits) - 1) * self.samples_per_symbol + 1, np.complex64
|
||||||
|
)
|
||||||
|
self.kernel[::self.samples_per_symbol] = np.conj(self.expected)
|
||||||
self.kernel = self.kernel[::-1].copy()
|
self.kernel = self.kernel[::-1].copy()
|
||||||
for values in (self.bits, self.reference, self.taps, self.expected, self.kernel):
|
for values in (self.bits, self.reference, self.taps, self.expected, self.kernel):
|
||||||
values.flags.writeable = False
|
values.flags.writeable = False
|
||||||
slow_offsets = bind(lab043._timing_offsets, **constants)
|
slow_offsets = bind(lab043._timing_offsets, **constants)
|
||||||
slow_timing = bind(previous.ORIGINAL_TIMING, _timing_offsets=slow_offsets, **constants)
|
slow_timing = bind(previous.ORIGINAL_TIMING, _timing_offsets=slow_offsets, **constants)
|
||||||
self.timing = measured("timing")(bind(previous.fast_preamble_timing,
|
self.timing = measured("timing")(bind(previous.fast_preamble_timing,
|
||||||
lab045=SimpleNamespace(SAMPLES_PER_SYMBOL=SAMPLES_PER_SYMBOL),
|
lab045=SimpleNamespace(SAMPLES_PER_SYMBOL=self.samples_per_symbol),
|
||||||
timing_kernel=lambda unused: self.kernel, ORIGINAL_TIMING=slow_timing))
|
timing_kernel=lambda unused: self.kernel, ORIGINAL_TIMING=slow_timing))
|
||||||
estimate = measured("cfo_estimation")(bind(lab043._estimate_cfo_gain, **constants))
|
estimate = measured("cfo_estimation")(bind(lab043._estimate_cfo_gain, **constants))
|
||||||
self.carrier = measured("phase_polarity")(bind(lab043._evaluate_symbols,
|
self.carrier = measured("phase_polarity")(bind(lab043._evaluate_symbols,
|
||||||
@@ -142,7 +167,20 @@ class PacketProcessor:
|
|||||||
|
|
||||||
@measured("frequency_shift")
|
@measured("frequency_shift")
|
||||||
def shift(self, segment):
|
def shift(self, segment):
|
||||||
return (segment - np.mean(segment)) * rotation(len(segment))
|
if self.carrier_tracking_mode == "legacy":
|
||||||
|
dc_reference = segment
|
||||||
|
else:
|
||||||
|
# Последний raw-отсчёт, способный попасть через причинный RRC в
|
||||||
|
# последнюю P3-точку преамбулы. Header в оценку DC не попадает.
|
||||||
|
fixed_prefix = (
|
||||||
|
16 * self.samples_per_symbol
|
||||||
|
+ self.taps.size
|
||||||
|
+ (carrier_tracking.PREAMBLE_SYMBOLS - 1) * self.samples_per_symbol
|
||||||
|
)
|
||||||
|
dc_reference = segment[:min(len(segment), fixed_prefix)]
|
||||||
|
return (segment - np.mean(dc_reference)) * rotation(
|
||||||
|
len(segment), self.sample_rate_hz
|
||||||
|
)
|
||||||
|
|
||||||
@measured("matched_rrc_filtering")
|
@measured("matched_rrc_filtering")
|
||||||
def filter(self, values):
|
def filter(self, values):
|
||||||
@@ -150,22 +188,55 @@ class PacketProcessor:
|
|||||||
|
|
||||||
@measured("soft_symbol_extraction")
|
@measured("soft_symbol_extraction")
|
||||||
def normalize(self, matched, nominal, timing, count, carrier):
|
def normalize(self, matched, nominal, timing, count, carrier):
|
||||||
symbols = matched[nominal + timing:nominal + timing + count * SAMPLES_PER_SYMBOL:SAMPLES_PER_SYMBOL].astype(np.complex128)
|
symbols = matched[
|
||||||
|
nominal + timing:
|
||||||
|
nominal + timing + count * self.samples_per_symbol:
|
||||||
|
self.samples_per_symbol
|
||||||
|
].astype(np.complex128)
|
||||||
if symbols.size != count:
|
if symbols.size != count:
|
||||||
return None
|
return None
|
||||||
symbol_time = (np.arange(count, dtype=np.float64) + .5) / SYMBOL_RATE
|
if self.carrier_tracking_mode == "legacy":
|
||||||
normalized = symbols * np.exp(-2j * math.pi * carrier["cfo_hz"] * symbol_time)
|
symbol_time = (
|
||||||
|
np.arange(count, dtype=np.float64) + 0.5
|
||||||
|
) / self.symbol_rate
|
||||||
|
normalized = symbols * np.exp(
|
||||||
|
-2j * math.pi * carrier["cfo_hz"] * symbol_time
|
||||||
|
)
|
||||||
normalized /= carrier["abs_h"] * np.exp(1j * carrier["phase_rad"])
|
normalized /= carrier["abs_h"] * np.exp(1j * carrier["phase_rad"])
|
||||||
return normalized.real
|
return normalized.real
|
||||||
|
|
||||||
|
if count < carrier_tracking.PREAMBLE_SYMBOLS:
|
||||||
|
return None
|
||||||
|
estimate = carrier_tracking.estimate_p3(
|
||||||
|
symbols[:carrier_tracking.PREAMBLE_SYMBOLS],
|
||||||
|
self.expected,
|
||||||
|
self.symbol_rate,
|
||||||
|
)
|
||||||
|
carrier.update({
|
||||||
|
"cfo_hz": estimate.frequency_hz,
|
||||||
|
"abs_h": float(abs(estimate.gain)),
|
||||||
|
"phase_rad": float(np.angle(estimate.gain)),
|
||||||
|
"polarity": "resolved from P3 full preamble",
|
||||||
|
})
|
||||||
|
result = carrier_tracking.track_t2(
|
||||||
|
symbols,
|
||||||
|
self.symbol_rate,
|
||||||
|
estimate,
|
||||||
|
start_index=carrier_tracking.PREAMBLE_SYMBOLS,
|
||||||
|
)
|
||||||
|
context = CURRENT.get()
|
||||||
|
if context is not None:
|
||||||
|
context.cache.setdefault("carrier_tracking_traces", []).append(result)
|
||||||
|
return result.corrected.real
|
||||||
|
|
||||||
@measured("extract_total")
|
@measured("extract_total")
|
||||||
def extract(self, samples, frame_start, symbol_count):
|
def extract(self, samples, frame_start, symbol_count):
|
||||||
length = (symbol_count + 42) * SAMPLES_PER_SYMBOL
|
length = (symbol_count + 42) * self.samples_per_symbol
|
||||||
if frame_start < 0 or frame_start + length > samples.size:
|
if frame_start < 0 or frame_start + length > samples.size:
|
||||||
return previous.iq.SoftWindow(False, np.empty(0), 0, 0, 0, {}, "TRUNCATED_IQ_WINDOW")
|
return previous.iq.SoftWindow(False, np.empty(0), 0, 0, 0, {}, "TRUNCATED_IQ_WINDOW")
|
||||||
segment = samples[frame_start:frame_start + length]
|
segment = samples[frame_start:frame_start + length]
|
||||||
matched = self.filter(self.shift(segment))
|
matched = self.filter(self.shift(segment))
|
||||||
nominal = 16 * SAMPLES_PER_SYMBOL + self.taps.size - 1
|
nominal = 16 * self.samples_per_symbol + self.taps.size - 1
|
||||||
timing = self.timing(matched, nominal, self.expected)
|
timing = self.timing(matched, nominal, self.expected)
|
||||||
carrier = self.carrier(matched, nominal, timing, self.bits, len(self.bits))
|
carrier = self.carrier(matched, nominal, timing, self.bits, len(self.bits))
|
||||||
if carrier["abs_h"] == 0:
|
if carrier["abs_h"] == 0:
|
||||||
@@ -174,7 +245,7 @@ class PacketProcessor:
|
|||||||
if values is None:
|
if values is None:
|
||||||
return previous.iq.SoftWindow(False, np.empty(0), timing, 0, nominal, carrier, "TRUNCATED_SYMBOLS")
|
return previous.iq.SoftWindow(False, np.empty(0), timing, 0, nominal, carrier, "TRUNCATED_SYMBOLS")
|
||||||
return previous.iq.SoftWindow(True, values, int(timing),
|
return previous.iq.SoftWindow(True, values, int(timing),
|
||||||
int((frame_start + nominal + timing) % SAMPLES_PER_SYMBOL), nominal, carrier, None)
|
int((frame_start + nominal + timing) % self.samples_per_symbol), nominal, carrier, None)
|
||||||
|
|
||||||
def decode(self, samples, candidate, number=1):
|
def decode(self, samples, candidate, number=1):
|
||||||
context = PacketMeasure()
|
context = PacketMeasure()
|
||||||
@@ -189,7 +260,11 @@ class PacketProcessor:
|
|||||||
"thread_cpu_seconds": time.thread_time() - cpu_started,
|
"thread_cpu_seconds": time.thread_time() - cpu_started,
|
||||||
"stages": dict(context.seconds), "exclusive_stages": dict(context.exclusive_seconds),
|
"stages": dict(context.seconds), "exclusive_stages": dict(context.exclusive_seconds),
|
||||||
"calls": dict(context.calls), "coarse": candidate.sample_offset,
|
"calls": dict(context.calls), "coarse": candidate.sample_offset,
|
||||||
"refined": outcome.refined_start_sample}
|
"refined": outcome.refined_start_sample,
|
||||||
|
"carrier_tracking_mode": self.carrier_tracking_mode,
|
||||||
|
"carrier_tracking_traces": tuple(
|
||||||
|
context.cache.get("carrier_tracking_traces", ())
|
||||||
|
)}
|
||||||
if outcome.frame is not None:
|
if outcome.frame is not None:
|
||||||
row.update(timing=outcome.frame.timing_offset_samples, cfo_hz=outcome.frame.estimated_cfo_hz,
|
row.update(timing=outcome.frame.timing_offset_samples, cfo_hz=outcome.frame.estimated_cfo_hz,
|
||||||
phase_rad=outcome.frame.estimated_phase_rad, polarity=outcome.frame.bpsk_polarity)
|
phase_rad=outcome.frame.estimated_phase_rad, polarity=outcome.frame.bpsk_polarity)
|
||||||
@@ -201,6 +276,7 @@ class PacketProcessor:
|
|||||||
def run_functional_tests() -> None:
|
def run_functional_tests() -> None:
|
||||||
processor = PacketProcessor()
|
processor = PacketProcessor()
|
||||||
assert len(processor.bits) == 1280 and len(processor.taps) == 161
|
assert len(processor.bits) == 1280 and len(processor.taps) == 161
|
||||||
|
assert processor.carrier_tracking_mode == "legacy"
|
||||||
assert not processor.reference.flags.writeable
|
assert not processor.reference.flags.writeable
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
260
experiments/lab061_strict_receiver_t2.py
Normal file
260
experiments/lab061_strict_receiver_t2.py
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
"""Lab061: офлайн-интеграция T2 в существующий строгий приёмник."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __package__ in (None, ""):
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from experiments import lab045_hackrf_bpsk_fec as lab045
|
||||||
|
from experiments import lab049_bpsk_symbol_rate as lab049
|
||||||
|
from experiments import lab052_candidate_detection as detection
|
||||||
|
from experiments import lab053_packet_processing as packet
|
||||||
|
from experiments import lab053_streaming_receiver as streaming
|
||||||
|
from experiments import lab058_abab_symbol_quality as lab058
|
||||||
|
from experiments import lab059_preamble_carrier_limits as lab059
|
||||||
|
from experiments import lab060_decision_directed_carrier_tracking as lab060
|
||||||
|
from protocol import bpsk_carrier_tracking as tracking
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
OUTPUT = ROOT / "data/processed/lab061"
|
||||||
|
FREQUENCIES = (-10, -5, -2, -1, -0.5, -0.25, 0, 0.25, 0.5, 1, 2, 5, 10)
|
||||||
|
QUALITIES = {"GOOD": 2.0, "MEDIUM": 24.0, "LAB056_B_LIKE": 105.0}
|
||||||
|
|
||||||
|
|
||||||
|
def write_csv(path: Path, rows: list[dict]) -> None:
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
columns = list(rows[0])
|
||||||
|
with path.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, columns)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def full_packet() -> tuple[np.ndarray, bytes, np.ndarray]:
|
||||||
|
payload = bytes(range(256))
|
||||||
|
plan = lab045.build_transmission_plan(payload)
|
||||||
|
raw = np.frombuffer(
|
||||||
|
lab049.build_parameterized_tx_cs8(plan, lab049.PhyMode(500_000)), np.int8
|
||||||
|
)
|
||||||
|
values = raw[::2].astype(np.float32) + 1j * raw[1::2].astype(np.float32)
|
||||||
|
expected = lab049.radio.bpsk_modulate(plan.frame_bits)
|
||||||
|
return values.astype(np.complex64), payload, expected
|
||||||
|
|
||||||
|
|
||||||
|
def stream_decode(values: np.ndarray, mode: str) -> tuple[list, streaming.StreamingReceiver, list[dict]]:
|
||||||
|
observed: list[dict] = []
|
||||||
|
receiver = streaming.StreamingReceiver(
|
||||||
|
processor=packet.PacketProcessor(carrier_tracking_mode=mode),
|
||||||
|
packet_observer=lambda row, outcome: observed.append({
|
||||||
|
"reason": outcome.reason,
|
||||||
|
"accepted": outcome.accepted,
|
||||||
|
"header": bool(outcome.phy_header_pass),
|
||||||
|
"fec": bool(outcome.fec_pass),
|
||||||
|
"crc": outcome.crc == "PASS",
|
||||||
|
"traces": row["carrier_tracking_traces"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
output = []
|
||||||
|
for start in range(0, len(values), receiver.block_samples):
|
||||||
|
output.extend(receiver.push_iq_block(values[start:start + receiver.block_samples]))
|
||||||
|
output.extend(receiver.finish())
|
||||||
|
return output, receiver, observed
|
||||||
|
|
||||||
|
|
||||||
|
def synthetic_study() -> tuple[list[dict], dict[str, np.ndarray]]:
|
||||||
|
clean, payload, expected = full_packet()
|
||||||
|
indexes = np.arange(len(clean))
|
||||||
|
rows: list[dict] = []
|
||||||
|
representative: dict[str, np.ndarray] = {}
|
||||||
|
for quality_number, (quality, noise_sigma) in enumerate(QUALITIES.items()):
|
||||||
|
for frequency in FREQUENCIES:
|
||||||
|
seed = 61_000 + quality_number * 100 + FREQUENCIES.index(frequency)
|
||||||
|
random_generator = np.random.default_rng(seed)
|
||||||
|
noise = random_generator.normal(0, noise_sigma, len(clean)) + 1j * random_generator.normal(0, noise_sigma, len(clean))
|
||||||
|
impaired = clean * np.exp(1j * (0.37 + 2 * np.pi * frequency * indexes / 8_000_000)) + noise
|
||||||
|
values = np.concatenate((np.zeros(20_000), impaired, np.zeros(20_000))).astype(np.complex64)
|
||||||
|
for mode in ("legacy", "t2"):
|
||||||
|
output, receiver, observed = stream_decode(values, mode)
|
||||||
|
header = any(row["header"] for row in observed)
|
||||||
|
fec = any(row["fec"] for row in observed)
|
||||||
|
crc = any(row["crc"] for row in observed)
|
||||||
|
traces = [trace for item in observed for trace in item["traces"]]
|
||||||
|
full_trace = max(traces, key=lambda item: len(item.corrected)) if traces else None
|
||||||
|
measured_evm = None
|
||||||
|
if full_trace is not None and len(full_trace.corrected) == len(expected):
|
||||||
|
selected = slice(1280, None)
|
||||||
|
gain = np.mean(full_trace.corrected[selected] * expected[selected])
|
||||||
|
residual = full_trace.corrected[selected] - gain * expected[selected]
|
||||||
|
measured_evm = float(np.sqrt(np.mean(abs(residual)**2) / abs(gain)**2))
|
||||||
|
rows.append({
|
||||||
|
"quality": quality, "noise_sigma": noise_sigma,
|
||||||
|
"initial_frequency_error_hz": frequency, "initial_phase_rad": 0.37,
|
||||||
|
"seed": seed, "mode": mode, "measured_evm_rms": measured_evm,
|
||||||
|
"candidates": receiver.candidate_count,
|
||||||
|
"HEADER_ACCEPTED": header, "FEC_STARTED": header,
|
||||||
|
"FEC_PASS": fec, "CRC_STARTED": fec, "CRC_PASS": crc,
|
||||||
|
"PACKET_OUTPUT": len(output) == 1 and output[0].payload == payload,
|
||||||
|
})
|
||||||
|
if mode == "t2" and quality == "LAB056_B_LIKE" and frequency == 10 and observed:
|
||||||
|
if traces:
|
||||||
|
trace = max(traces, key=lambda item: len(item.phase_rad))
|
||||||
|
representative = {
|
||||||
|
"phase_rad": trace.phase_rad,
|
||||||
|
"frequency_hz": trace.frequency_hz,
|
||||||
|
"detector_error_rad": trace.detector_error_rad,
|
||||||
|
}
|
||||||
|
return rows, representative
|
||||||
|
|
||||||
|
|
||||||
|
def decode_known_candidates() -> list[dict]:
|
||||||
|
lab054_items, lab047_items, lab054_reader, lab047_reader = lab059._packet_sources()
|
||||||
|
rows: list[dict] = []
|
||||||
|
for dataset, symbol_rate, items, reader, key in (
|
||||||
|
("Lab047", 20_000, lab047_items, lab047_reader, "candidate_offset"),
|
||||||
|
("Lab054", 500_000, lab054_items, lab054_reader, "coarse_start"),
|
||||||
|
):
|
||||||
|
for mode in ("legacy", "t2"):
|
||||||
|
processor = packet.PacketProcessor(carrier_tracking_mode=mode, symbol_rate=symbol_rate)
|
||||||
|
for number, item in enumerate(items, 1):
|
||||||
|
start = int(item[key])
|
||||||
|
low = max(0, start - 4_000)
|
||||||
|
length = 2_400_000 if symbol_rate == 20_000 else 120_000
|
||||||
|
values = reader.read(low, min(length, reader.size - low))
|
||||||
|
candidate = detection.offline.CoarseCandidate(start - low, 1.0)
|
||||||
|
outcome, detail = processor.decode(values, candidate, number)
|
||||||
|
rows.append({
|
||||||
|
"dataset": dataset, "frame": number, "mode": mode,
|
||||||
|
"HEADER_ACCEPTED": outcome.phy_header_pass,
|
||||||
|
"FEC_STARTED": outcome.phy_header_pass,
|
||||||
|
"FEC_PASS": outcome.fec_pass,
|
||||||
|
"CRC_STARTED": outcome.fec_pass,
|
||||||
|
"CRC_PASS": outcome.crc == "PASS", "PACKET_OUTPUT": outcome.accepted,
|
||||||
|
"reason": outcome.reason, "initial_frequency_hz": detail.get("cfo_hz"),
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def lab056_study() -> tuple[list[dict], dict[str, np.ndarray]]:
|
||||||
|
rows, trajectories = [], {}
|
||||||
|
for item in lab060._lab056_data():
|
||||||
|
source = item["estimate"]
|
||||||
|
estimate = tracking.P3CarrierEstimate(
|
||||||
|
source.frequency_hz, source.gain, source.phase_fit_rmse_rad, source.mean_block_coherence
|
||||||
|
)
|
||||||
|
result = tracking.track_t2(item["symbols"], item["symbol_rate"], estimate)
|
||||||
|
compatible = lab060.TrackResult(
|
||||||
|
tracker="T2", corrected=result.corrected,
|
||||||
|
hard_decisions=result.hard_decisions, phase_rad=result.phase_rad,
|
||||||
|
frequency_hz=result.frequency_hz,
|
||||||
|
detector_error_rad=result.detector_error_rad,
|
||||||
|
parameters=lab060.loop_parameters("T2", 0.0005),
|
||||||
|
initial_frequency_hz=estimate.frequency_hz,
|
||||||
|
initial_phase_rad=float(np.angle(estimate.gain)),
|
||||||
|
)
|
||||||
|
metrics, blocks = lab060.evaluate_tracking(compatible, item["expected"], item["symbol_rate"])
|
||||||
|
rows.append({"label": item["label"], "symbol_rate": item["symbol_rate"], **{k: v for k, v in metrics.items() if k != "receiver_lock"}, "stable_lock": metrics["receiver_lock"]["stable_lock"]})
|
||||||
|
trajectories[f"{item['label']}_phase_rad"] = result.phase_rad
|
||||||
|
trajectories[f"{item['label']}_frequency_hz"] = result.frequency_hz
|
||||||
|
return rows, trajectories
|
||||||
|
|
||||||
|
|
||||||
|
def background_study() -> dict:
|
||||||
|
source = json.loads((ROOT / "data/processed/lab060/summary.json").read_text(encoding="utf-8"))
|
||||||
|
background = source["background"][0]
|
||||||
|
start, count = int(background["rx_start"]), int(background["samples"])
|
||||||
|
reader = lab058.Cs8File(ROOT / "data/raw/lab054/rx_stream.cs8")
|
||||||
|
values = reader.read(start, count)
|
||||||
|
results = {}
|
||||||
|
for mode in ("legacy", "t2"):
|
||||||
|
output, receiver, observed = stream_decode(values, mode)
|
||||||
|
results[mode] = {"headers": sum(row["header"] for row in observed), "packets": len(output), "candidates": receiver.candidate_count}
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def saved_tx_control() -> dict:
|
||||||
|
reader = lab058.Cs8File(ROOT / "data/raw/lab054/tx_stream.cs8")
|
||||||
|
values = reader.read(0, reader.size)
|
||||||
|
results = {}
|
||||||
|
for mode in ("legacy", "t2"):
|
||||||
|
output, receiver, observed = stream_decode(values, mode)
|
||||||
|
results[mode] = {"headers": sum(row["header"] for row in observed), "packets": len(output)}
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def robustness() -> list[dict]:
|
||||||
|
symbols, expected, unused = lab060.clean_rrc_symbols(500_000)
|
||||||
|
estimate = tracking.estimate_p3(symbols[:1280], expected[:1280], 500_000)
|
||||||
|
cases = {}
|
||||||
|
for name, selected in (("single", slice(2000, 2001)), ("short", slice(2500, 2510)), ("long", slice(3000, 3080))):
|
||||||
|
damaged = symbols.astype(np.complex128)
|
||||||
|
damaged[selected] *= -1
|
||||||
|
cases[name] = damaged
|
||||||
|
fade = symbols.astype(np.complex128); fade[3500:3700] *= 0.02; cases["amplitude_drop"] = fade
|
||||||
|
noise = symbols.astype(np.complex128); random_generator = np.random.default_rng(61999); noise[4000:4300] = random_generator.normal(size=300) + 1j*random_generator.normal(size=300); cases["noise"] = noise
|
||||||
|
rows = []
|
||||||
|
for name, values in cases.items():
|
||||||
|
result = tracking.track_t2(values, 500_000, estimate)
|
||||||
|
rows.append({"case": name, "finite": bool(np.all(np.isfinite(result.corrected)) and np.all(np.isfinite(result.frequency_hz))), "max_abs_frequency_hz": float(np.max(abs(result.frequency_hz)))})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def plots(synthetic: list[dict], representative: dict, real: list[dict], background: dict) -> None:
|
||||||
|
plt.figure(); plt.plot(np.arange(len(representative["phase_rad"]))/500_000, representative["phase_rad"]); plt.xlabel("Время, с"); plt.ylabel("Фаза T2, рад"); plt.tight_layout(); plt.savefig(OUTPUT/"synthetic_t2_phase.png", dpi=160); plt.close()
|
||||||
|
plt.figure(); plt.plot(np.arange(len(representative["frequency_hz"]))/500_000, representative["frequency_hz"]); plt.xlabel("Время, с"); plt.ylabel("Частотное состояние, Гц"); plt.tight_layout(); plt.savefig(OUTPUT/"synthetic_t2_frequency.png", dpi=160); plt.close()
|
||||||
|
for mode in ("legacy", "t2"):
|
||||||
|
values = [np.mean([r["PACKET_OUTPUT"] for r in synthetic if r["mode"] == mode and r["initial_frequency_error_hz"] == f]) for f in FREQUENCIES]
|
||||||
|
plt.plot(FREQUENCIES, values, marker="o", label=mode)
|
||||||
|
plt.xlabel("Начальная ошибка частоты, Гц"); plt.ylabel("Доля пакетов"); plt.legend(); plt.tight_layout(); plt.savefig(OUTPUT/"packet_success_by_initial_cfo.png", dpi=160); plt.close()
|
||||||
|
for dataset, filename in (("Lab047", "lab047_regression.png"), ("Lab054", "lab054_strict_result.png")):
|
||||||
|
values = [sum(r["PACKET_OUTPUT"] for r in real if r["dataset"] == dataset and r["mode"] == mode) for mode in ("legacy", "t2")]
|
||||||
|
plt.figure(); plt.bar(("legacy", "t2"), values); plt.ylabel("CRC PASS пакеты"); plt.tight_layout(); plt.savefig(OUTPUT/filename, dpi=160); plt.close()
|
||||||
|
plt.figure(); plt.bar(("legacy headers", "legacy packets", "T2 headers", "T2 packets"), (background["legacy"]["headers"], background["legacy"]["packets"], background["t2"]["headers"], background["t2"]["packets"])); plt.ylabel("Количество"); plt.tight_layout(); plt.savefig(OUTPUT/"background_result.png", dpi=160); plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
synthetic, representative = synthetic_study()
|
||||||
|
real = decode_known_candidates()
|
||||||
|
lab056, lab056_trajectories = lab056_study()
|
||||||
|
background = background_study()
|
||||||
|
tx_control = saved_tx_control()
|
||||||
|
robust = robustness()
|
||||||
|
write_csv(OUTPUT/"synthetic_packet_results.csv", synthetic)
|
||||||
|
write_csv(OUTPUT/"real_results.csv", real)
|
||||||
|
write_csv(OUTPUT/"lab056_results.csv", lab056)
|
||||||
|
write_csv(OUTPUT/"robustness_results.csv", robust)
|
||||||
|
np.savez_compressed(OUTPUT/"trajectories.npz", **representative, **lab056_trajectories)
|
||||||
|
plots(synthetic, representative, real, background)
|
||||||
|
by_mode = {mode: {"header_pass_rate": float(np.mean([r["HEADER_ACCEPTED"] for r in synthetic if r["mode"] == mode])), "crc_pass_rate": float(np.mean([r["CRC_PASS"] for r in synthetic if r["mode"] == mode]))} for mode in ("legacy", "t2")}
|
||||||
|
by_frequency = {str(f): float(np.mean([r["PACKET_OUTPUT"] for r in synthetic if r["mode"] == "t2" and r["initial_frequency_error_hz"] == f])) for f in FREQUENCIES}
|
||||||
|
summary = {
|
||||||
|
"lab": "Lab061", "status": "COMPLETED", "scope": "OFFLINE", "rf_tx": 0,
|
||||||
|
"hardware_queries": 0, "legacy_default": True, "automatic_fallback": False,
|
||||||
|
"t2_parameters": vars(tracking.t2_parameters()),
|
||||||
|
"synthetic": by_mode, "t2_packet_success_by_initial_frequency_error": by_frequency,
|
||||||
|
"real": {dataset: {mode: sum(r["PACKET_OUTPUT"] for r in real if r["dataset"] == dataset and r["mode"] == mode) for mode in ("legacy", "t2")} for dataset in ("Lab047", "Lab054")},
|
||||||
|
"lab056": lab056, "background": background, "saved_tx_control": tx_control,
|
||||||
|
"robustness": robust,
|
||||||
|
"payload_truth_leak_test": "PASS: strict API and T2 have no reference payload input; causal prefix test PASS",
|
||||||
|
"production_decision": "READY_BEHIND_EXPLICIT_MODE",
|
||||||
|
"physical_packet": {"bytes": 268, "symbols": 5724, "duration_seconds": len(full_packet()[0])/8_000_000},
|
||||||
|
"source_sha256": {str(path.relative_to(ROOT)): hashlib.sha256(path.read_bytes()).hexdigest() for path in (ROOT/"data/raw/lab054/rx_stream.cs8",)},
|
||||||
|
}
|
||||||
|
(OUTPUT/"summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2, default=lambda value: value.item()), encoding="utf-8")
|
||||||
|
print(json.dumps(summary, ensure_ascii=False, indent=2, default=lambda value: value.item()))
|
||||||
|
print("RF TX = 0")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
230
protocol/bpsk_carrier_tracking.py
Normal file
230
protocol/bpsk_carrier_tracking.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
"""Причинное сопровождение несущей BPSK после известной преамбулы.
|
||||||
|
|
||||||
|
Модуль не разбирает заголовок, полезную нагрузку, FEC или CRC. Начальное
|
||||||
|
состояние P3 оценивается только по переданным в функцию символам преамбулы.
|
||||||
|
После неё T2 получает текущий комплексный символ, собственное решение и своё
|
||||||
|
предыдущее состояние.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import math
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
PREAMBLE_SYMBOLS = 1280
|
||||||
|
P3_BLOCK_SYMBOLS = 20
|
||||||
|
NORMALIZED_LOOP_BANDWIDTH = 0.0005
|
||||||
|
DAMPING_FACTOR = 1.0 / math.sqrt(2.0)
|
||||||
|
DETECTOR_CLIP_RAD = math.pi / 3.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class P3CarrierEstimate:
|
||||||
|
"""Начальное состояние в физических единицах при времени символа 0."""
|
||||||
|
|
||||||
|
frequency_hz: float
|
||||||
|
gain: complex
|
||||||
|
phase_fit_rmse_rad: float
|
||||||
|
squared_coherence: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class T2Parameters:
|
||||||
|
"""Коэффициенты петли на один символ."""
|
||||||
|
|
||||||
|
normalized_bandwidth: float
|
||||||
|
damping_factor: float
|
||||||
|
phase_coefficient: float
|
||||||
|
frequency_coefficient: float
|
||||||
|
detector_clip_rad: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class T2TrackingResult:
|
||||||
|
"""Применённое состояние и решения для одного последовательного прохода."""
|
||||||
|
|
||||||
|
corrected: np.ndarray
|
||||||
|
hard_decisions: np.ndarray
|
||||||
|
phase_rad: np.ndarray
|
||||||
|
frequency_hz: np.ndarray
|
||||||
|
detector_error_rad: np.ndarray
|
||||||
|
initial_estimate: P3CarrierEstimate
|
||||||
|
parameters: T2Parameters
|
||||||
|
|
||||||
|
|
||||||
|
def t2_parameters(
|
||||||
|
normalized_bandwidth: float = NORMALIZED_LOOP_BANDWIDTH,
|
||||||
|
damping_factor: float = DAMPING_FACTOR,
|
||||||
|
) -> T2Parameters:
|
||||||
|
"""Перевести нормированную полосу в коэффициенты Lab060 на символ."""
|
||||||
|
|
||||||
|
if not np.isfinite(normalized_bandwidth) or normalized_bandwidth <= 0.0:
|
||||||
|
raise ValueError("Нормированная полоса должна быть положительной")
|
||||||
|
if not np.isfinite(damping_factor) or damping_factor <= 0.0:
|
||||||
|
raise ValueError("Коэффициент затухания должен быть положительным")
|
||||||
|
theta = normalized_bandwidth / (
|
||||||
|
damping_factor + 0.25 / damping_factor
|
||||||
|
)
|
||||||
|
denominator = 1.0 + 2.0 * damping_factor * theta + theta * theta
|
||||||
|
return T2Parameters(
|
||||||
|
normalized_bandwidth=float(normalized_bandwidth),
|
||||||
|
damping_factor=float(damping_factor),
|
||||||
|
phase_coefficient=float(4.0 * damping_factor * theta / denominator),
|
||||||
|
frequency_coefficient=float(4.0 * theta * theta / denominator),
|
||||||
|
detector_clip_rad=DETECTOR_CLIP_RAD,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _huber_phase_line(
|
||||||
|
block_values: np.ndarray,
|
||||||
|
block_times_seconds: np.ndarray,
|
||||||
|
) -> tuple[float, float, float, float]:
|
||||||
|
"""Повторить зафиксированную в P3 устойчивую прямую фазы."""
|
||||||
|
|
||||||
|
values = np.asarray(block_values, dtype=np.complex128)
|
||||||
|
times = np.asarray(block_times_seconds, dtype=np.float64)
|
||||||
|
if values.ndim != 1 or times.ndim != 1 or len(values) != len(times) or len(values) < 3:
|
||||||
|
raise ValueError("Недостаточно фазовых блоков")
|
||||||
|
phases = np.unwrap(np.angle(values))
|
||||||
|
base_weights = np.maximum(np.abs(values), 1e-12)
|
||||||
|
center_time = float(np.sum(base_weights * times) / np.sum(base_weights))
|
||||||
|
centered = times - center_time
|
||||||
|
design = np.column_stack((np.ones(len(times)), centered))
|
||||||
|
weights = base_weights.copy()
|
||||||
|
coefficients = np.zeros(2, dtype=np.float64)
|
||||||
|
for _iteration in range(12):
|
||||||
|
root = np.sqrt(weights / max(float(np.max(weights)), 1e-30))
|
||||||
|
updated, *_unused = np.linalg.lstsq(
|
||||||
|
design * root[:, None], phases * root, rcond=None
|
||||||
|
)
|
||||||
|
residual = phases - design @ updated
|
||||||
|
median = float(np.median(residual))
|
||||||
|
scale = 1.4826 * float(np.median(np.abs(residual - median))) + 1e-9
|
||||||
|
limit = 1.345 * scale
|
||||||
|
robust = np.ones_like(residual)
|
||||||
|
outside = np.abs(residual) > limit
|
||||||
|
robust[outside] = limit / np.abs(residual[outside])
|
||||||
|
weights = base_weights * robust
|
||||||
|
if np.allclose(updated, coefficients, rtol=0.0, atol=1e-12):
|
||||||
|
coefficients = updated
|
||||||
|
break
|
||||||
|
coefficients = updated
|
||||||
|
residual = phases - design @ coefficients
|
||||||
|
rmse = float(np.sqrt(np.sum(base_weights * residual**2) / np.sum(base_weights)))
|
||||||
|
phase_at_zero = float(coefficients[0] - coefficients[1] * center_time)
|
||||||
|
coherence = float(abs(np.sum(values)) / max(float(np.sum(abs(values))), 1e-30))
|
||||||
|
return phase_at_zero, float(coefficients[1]), rmse, coherence
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_p3(
|
||||||
|
preamble_symbols: np.ndarray,
|
||||||
|
known_preamble_symbols: np.ndarray,
|
||||||
|
symbol_rate: float,
|
||||||
|
) -> P3CarrierEstimate:
|
||||||
|
"""Оценить P3 только по полной 1280-символьной преамбуле."""
|
||||||
|
|
||||||
|
received = np.asarray(preamble_symbols, dtype=np.complex128)
|
||||||
|
known = np.asarray(known_preamble_symbols, dtype=np.complex128)
|
||||||
|
if received.shape != (PREAMBLE_SYMBOLS,) or known.shape != received.shape:
|
||||||
|
raise ValueError("P3 требуется ровно 1280 принятых и известных символов")
|
||||||
|
if not np.isfinite(symbol_rate) or symbol_rate <= 0.0:
|
||||||
|
raise ValueError("Скорость символов должна быть положительной")
|
||||||
|
if not np.all(np.isfinite(received)) or not np.all(np.isfinite(known)):
|
||||||
|
raise ValueError("P3 не принимает NaN или Inf")
|
||||||
|
|
||||||
|
indexes = np.arange(PREAMBLE_SYMBOLS, dtype=np.int64)
|
||||||
|
block_count = PREAMBLE_SYMBOLS // P3_BLOCK_SYMBOLS
|
||||||
|
block_values = np.mean(
|
||||||
|
(received**2).reshape(block_count, P3_BLOCK_SYMBOLS), axis=1
|
||||||
|
)
|
||||||
|
block_times = np.mean(
|
||||||
|
((indexes + 0.5) / symbol_rate).reshape(block_count, P3_BLOCK_SYMBOLS),
|
||||||
|
axis=1,
|
||||||
|
)
|
||||||
|
_phase0, squared_slope, rmse, coherence = _huber_phase_line(
|
||||||
|
block_values, block_times
|
||||||
|
)
|
||||||
|
frequency_hz = float(squared_slope / (4.0 * math.pi))
|
||||||
|
times = (indexes + 0.5) / symbol_rate
|
||||||
|
gain = complex(np.mean(
|
||||||
|
received
|
||||||
|
* np.conj(known)
|
||||||
|
* np.exp(-2j * math.pi * frequency_hz * times)
|
||||||
|
))
|
||||||
|
if not np.isfinite(gain.real) or not np.isfinite(gain.imag) or abs(gain) == 0.0:
|
||||||
|
raise ValueError("P3 получил нулевой или неконечный коэффициент")
|
||||||
|
return P3CarrierEstimate(frequency_hz, gain, rmse, coherence)
|
||||||
|
|
||||||
|
|
||||||
|
def normalized_phase_detector(
|
||||||
|
corrected_symbol: complex,
|
||||||
|
detector_clip_rad: float = DETECTOR_CLIP_RAD,
|
||||||
|
) -> tuple[int, float]:
|
||||||
|
"""Вернуть собственное решение BPSK и ограниченную фазовую ошибку."""
|
||||||
|
|
||||||
|
value = complex(corrected_symbol)
|
||||||
|
if not np.isfinite(value.real) or not np.isfinite(value.imag):
|
||||||
|
raise ValueError("Фазовый детектор не принимает NaN или Inf")
|
||||||
|
hard = 1 if value.real >= 0.0 else -1
|
||||||
|
error = math.atan2(value.imag * hard, value.real * hard)
|
||||||
|
return hard, float(np.clip(error, -detector_clip_rad, detector_clip_rad))
|
||||||
|
|
||||||
|
|
||||||
|
def track_t2(
|
||||||
|
symbols: np.ndarray,
|
||||||
|
symbol_rate: float,
|
||||||
|
initial_estimate: P3CarrierEstimate,
|
||||||
|
*,
|
||||||
|
start_index: int = PREAMBLE_SYMBOLS,
|
||||||
|
parameters: T2Parameters | None = None,
|
||||||
|
) -> T2TrackingResult:
|
||||||
|
"""Последовательно применить T2; будущие символы не используются."""
|
||||||
|
|
||||||
|
values = np.asarray(symbols, dtype=np.complex128)
|
||||||
|
if values.ndim != 1 or not len(values) or not np.all(np.isfinite(values)):
|
||||||
|
raise ValueError("T2 требуется конечная одномерная последовательность")
|
||||||
|
if not np.isfinite(symbol_rate) or symbol_rate <= 0.0:
|
||||||
|
raise ValueError("Скорость символов должна быть положительной")
|
||||||
|
if not 0 <= start_index <= len(values):
|
||||||
|
raise ValueError("Некорректная граница начала сопровождения")
|
||||||
|
if abs(initial_estimate.gain) == 0.0:
|
||||||
|
raise ValueError("Начальный коэффициент P3 не может быть нулевым")
|
||||||
|
selected = t2_parameters() if parameters is None else parameters
|
||||||
|
|
||||||
|
corrected = np.empty(len(values), dtype=np.complex128)
|
||||||
|
decisions = np.empty(len(values), dtype=np.int8)
|
||||||
|
phases = np.empty(len(values), dtype=np.float64)
|
||||||
|
frequencies = np.empty(len(values), dtype=np.float64)
|
||||||
|
detector = np.empty(len(values), dtype=np.float64)
|
||||||
|
omega = 2.0 * math.pi * initial_estimate.frequency_hz / symbol_rate
|
||||||
|
phase0 = float(np.angle(initial_estimate.gain))
|
||||||
|
phase = phase0 + omega * (start_index + 0.5)
|
||||||
|
amplitude = abs(initial_estimate.gain)
|
||||||
|
|
||||||
|
for index, symbol in enumerate(values):
|
||||||
|
current_phase = phase0 + omega * (index + 0.5) if index < start_index else phase
|
||||||
|
value = symbol * np.exp(-1j * current_phase) / amplitude
|
||||||
|
hard, error = normalized_phase_detector(value, selected.detector_clip_rad)
|
||||||
|
corrected[index] = value
|
||||||
|
decisions[index] = hard
|
||||||
|
phases[index] = current_phase
|
||||||
|
frequencies[index] = omega * symbol_rate / (2.0 * math.pi)
|
||||||
|
detector[index] = error
|
||||||
|
if index >= start_index:
|
||||||
|
phase = phase + omega + selected.phase_coefficient * error
|
||||||
|
omega = omega + selected.frequency_coefficient * error
|
||||||
|
|
||||||
|
for array in (corrected, decisions, phases, frequencies, detector):
|
||||||
|
array.flags.writeable = False
|
||||||
|
return T2TrackingResult(
|
||||||
|
corrected=corrected,
|
||||||
|
hard_decisions=decisions,
|
||||||
|
phase_rad=phases,
|
||||||
|
frequency_hz=frequencies,
|
||||||
|
detector_error_rad=detector,
|
||||||
|
initial_estimate=initial_estimate,
|
||||||
|
parameters=selected,
|
||||||
|
)
|
||||||
130
tests/test_lab061_strict_receiver_t2.py
Normal file
130
tests/test_lab061_strict_receiver_t2.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""Быстрые проверки интеграции T2 в строгий приёмник Lab061."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from experiments import lab045_hackrf_bpsk_fec as lab045
|
||||||
|
from experiments import lab049_bpsk_symbol_rate as lab049
|
||||||
|
from experiments import lab053_packet_processing as packet
|
||||||
|
from experiments import lab053_streaming_receiver as streaming
|
||||||
|
from experiments import lab059_preamble_carrier_limits as lab059
|
||||||
|
from experiments import lab060_decision_directed_carrier_tracking as lab060
|
||||||
|
from protocol import bpsk_carrier_tracking as tracking
|
||||||
|
|
||||||
|
|
||||||
|
def full_packet(payload: bytes = bytes(range(256))) -> np.ndarray:
|
||||||
|
plan = lab045.build_transmission_plan(payload)
|
||||||
|
mode = lab049.PhyMode(500_000)
|
||||||
|
raw = np.frombuffer(lab049.build_parameterized_tx_cs8(plan, mode), np.int8)
|
||||||
|
return raw[::2].astype(np.float32) + 1j * raw[1::2].astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def receive(values: np.ndarray, mode: str):
|
||||||
|
receiver = streaming.StreamingReceiver(
|
||||||
|
processor=packet.PacketProcessor(carrier_tracking_mode=mode)
|
||||||
|
)
|
||||||
|
output = []
|
||||||
|
for start in range(0, len(values), receiver.block_samples):
|
||||||
|
output.extend(receiver.push_iq_block(values[start:start + receiver.block_samples]))
|
||||||
|
output.extend(receiver.finish())
|
||||||
|
return output, receiver
|
||||||
|
|
||||||
|
|
||||||
|
def impaired(cfo_hz: float, seed: int = 61001) -> np.ndarray:
|
||||||
|
clean = full_packet()
|
||||||
|
indexes = np.arange(len(clean))
|
||||||
|
signal = clean * np.exp(1j * (0.37 + 2 * np.pi * cfo_hz * indexes / 8_000_000))
|
||||||
|
random_generator = np.random.default_rng(seed)
|
||||||
|
noise = random_generator.normal(0, 2.0, len(clean)) + 1j * random_generator.normal(0, 2.0, len(clean))
|
||||||
|
return np.concatenate((np.zeros(20_000, np.complex64), signal + noise, np.zeros(20_000, np.complex64))).astype(np.complex64)
|
||||||
|
|
||||||
|
|
||||||
|
def test_t2_coefficients_and_frequency_units():
|
||||||
|
parameters = tracking.t2_parameters()
|
||||||
|
assert parameters.phase_coefficient == pytest.approx(0.0013324447407406753)
|
||||||
|
assert parameters.frequency_coefficient == pytest.approx(8.882964938271169e-7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_p3_and_t2_are_numerically_equivalent_to_lab060():
|
||||||
|
symbols, expected, metadata = lab060.clean_rrc_symbols(500_000)
|
||||||
|
p3 = lab059.estimate_from_burst(symbols, 500_000, "P3")
|
||||||
|
production = tracking.P3CarrierEstimate(
|
||||||
|
p3.frequency_hz, p3.gain, p3.phase_fit_rmse_rad, p3.mean_block_coherence
|
||||||
|
)
|
||||||
|
actual = tracking.track_t2(symbols, 500_000, production)
|
||||||
|
reference = lab060.track_symbols(symbols, 500_000, p3.frequency_hz, p3.gain, "T2", lab060.loop_parameters("T2", 0.0005))
|
||||||
|
assert metadata["symbols"] == len(symbols)
|
||||||
|
assert np.allclose(actual.corrected, reference.corrected, rtol=0, atol=3e-15)
|
||||||
|
assert np.array_equal(actual.hard_decisions, reference.hard_decisions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_is_explicit_legacy_and_invalid_mode_is_rejected():
|
||||||
|
assert packet.PacketProcessor().carrier_tracking_mode == "legacy"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
packet.PacketProcessor(carrier_tracking_mode="automatic")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", ["legacy", "t2"])
|
||||||
|
def test_clean_full_500k_packet_passes_header_fec_crc(mode):
|
||||||
|
output, receiver = receive(impaired(0.0), mode)
|
||||||
|
assert len(output) == receiver.packet_count == 1
|
||||||
|
assert output[0].payload == bytes(range(256))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("cfo_hz", [-10.0, -2.0, 2.0, 10.0])
|
||||||
|
def test_t2_full_packet_with_positive_and_negative_residual_frequency(cfo_hz):
|
||||||
|
output, receiver = receive(impaired(cfo_hz), "t2")
|
||||||
|
assert len(output) == receiver.packet_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_t2_is_causal_and_payload_truth_is_not_an_input():
|
||||||
|
symbols, expected, unused = lab060.clean_rrc_symbols(500_000)
|
||||||
|
p3 = tracking.estimate_p3(symbols[:1280], expected[:1280], 500_000)
|
||||||
|
first = tracking.track_t2(symbols, 500_000, p3)
|
||||||
|
changed = symbols.copy()
|
||||||
|
changed[-1] *= -1
|
||||||
|
second = tracking.track_t2(changed, 500_000, p3)
|
||||||
|
assert np.array_equal(first.hard_decisions[:-1], second.hard_decisions[:-1])
|
||||||
|
assert np.array_equal(first.phase_rad[:-1], second.phase_rad[:-1])
|
||||||
|
|
||||||
|
|
||||||
|
def test_p3_frontend_does_not_read_raw_header_or_payload():
|
||||||
|
processor = packet.PacketProcessor(carrier_tracking_mode="t2")
|
||||||
|
original = full_packet().astype(np.complex128)
|
||||||
|
changed = original.copy()
|
||||||
|
boundary = 16 * processor.samples_per_symbol + processor.taps.size + 1279 * processor.samples_per_symbol
|
||||||
|
changed[boundary:] += 10.0 + 7.0j
|
||||||
|
def p3(values):
|
||||||
|
matched = processor.filter(processor.shift(values))
|
||||||
|
nominal = 16 * processor.samples_per_symbol + processor.taps.size - 1
|
||||||
|
received = matched[nominal:nominal + 1280 * processor.samples_per_symbol:processor.samples_per_symbol]
|
||||||
|
return tracking.estimate_p3(received, processor.expected, processor.symbol_rate)
|
||||||
|
first, second = p3(original), p3(changed)
|
||||||
|
assert first.frequency_hz == pytest.approx(second.frequency_hz, abs=1e-8)
|
||||||
|
assert first.gain == pytest.approx(second.gain, abs=1e-8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_bursts_fade_and_noise_remain_finite():
|
||||||
|
symbols, expected, unused = lab060.clean_rrc_symbols(500_000)
|
||||||
|
p3 = tracking.estimate_p3(symbols[:1280], expected[:1280], 500_000)
|
||||||
|
damaged = symbols.astype(np.complex128)
|
||||||
|
damaged[2000] *= -1
|
||||||
|
damaged[2600:2610] *= -1
|
||||||
|
damaged[3200:3280] *= -1
|
||||||
|
damaged[4000:4200] *= 0.02
|
||||||
|
random_generator = np.random.default_rng(61002)
|
||||||
|
damaged[4500:4700] = random_generator.normal(size=200) + 1j * random_generator.normal(size=200)
|
||||||
|
result = tracking.track_t2(damaged, 500_000, p3)
|
||||||
|
assert np.all(np.isfinite(result.corrected))
|
||||||
|
assert np.all(np.isfinite(result.frequency_hz))
|
||||||
|
assert np.max(np.abs(result.frequency_hz)) < 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_produces_no_header_or_packet_and_seed_is_deterministic():
|
||||||
|
one = np.random.default_rng(61003).normal(size=180_000).astype(np.float32).astype(np.complex64)
|
||||||
|
two = np.random.default_rng(61003).normal(size=180_000).astype(np.float32).astype(np.complex64)
|
||||||
|
assert np.array_equal(one, two)
|
||||||
|
output, receiver = receive(one, "t2")
|
||||||
|
assert output == [] and receiver.packet_count == 0
|
||||||
Reference in New Issue
Block a user