Files
SDR-Rover/docs/lab046_current_state.md
2026-09-01 17:09:27 +03:00

478 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lab046: текущее состояние
## Статус
**LAB046 SOFTWARE OBJECT TRANSPORT = COMPLETE.**
**CONTINUOUS STREAM VALIDATION = PASS (NORMALIZED SOFT-SYMBOL LEVEL).**
**ПРОГРАММНАЯ ПРОВЕРКА НЕПРЕРЫВНОГО СЫРОГО IQ-ПОТОКА = PASS.**
**LAB046C HARDWARE RESULT = C.**
**RF TX COUNT = 1.**
**PREAMBLE CANDIDATES = 0. VALID FRAMES = 0. CRC PASS = 0.**
**OBJECT COMPLETE = NO.**
**POST-RUN TX-REFERENCE DIAGNOSTIC = 68/68 WAVEFORM TRACES DETECTED.**
**OFFICIAL RESULT CHANGED BY POST-RUN DIAGNOSTICS = NO.**
**OFFLINE DETECTOR STUDY = COMPLETE.**
**OFFLINE REPROCESSING OF ORIGINAL RX CAPTURE = FAIL.**
**OFFLINE VALID FRAMES = 0. OFFLINE OBJECT COMPLETE = NO.**
**ORIGINAL HARDWARE RESULT CHANGED = NO.**
**CURRENT SOFTWARE GATE = PASS: quick_gate 506, pytest 506 passed,
git diff --check clean.**
Lab046A/B остаются только программными лабами. Для Lab046C выполнен ровно один
заранее разрешённый непрерывный RF-сеанс без повтора. Ни JPEG, ни видео, ни
QPSK не запускались.
## Lab046C: подготовка аппаратного непрерывного объекта
Добавлен `experiments/lab046c_hackrf_bpsk_object.py`, который не меняет
протокол и только связывает готовые Lab043--Lab046B. Подготовительный режим
создаёт новый криптографически случайный объект 16384 байта вне Git, разбивает
его существующим Lab046 API на 68 фрагментов и последовательно записывает
68 полных Lab045 waveform в один `tx_stream.cs8`. Полные 313 670 400 байт не
удерживаются в ОЗУ одновременно; SHA-256 объекта и потока сохраняются в
`tx_metadata.json`.
24-секундный RX был запланирован как 192 000 000 complex cs8 samples,
384 000 000 байт. Такой capture даёт 0,75 с до передачи и около 3,65 с после
потока длительностью 19,6044 с. Приёмный API получает только путь к сырому
cs8 и PHY safety limits. Он читает файл через memmap окнами `complex64`, ищет
локальные PRBS7-кандидаты без обязательного global peak/second gate, получает
длину кадра из принятого PHY header и передаёт прошедшие CRC payload в
`ObjectAssembler`. TX start, границы 68 waveform, ожидаемое число фрагментов,
пакеты, coded bits и исходный объект приёмнику не передаются.
Аппаратный runner фиксирует доказанные serial и PHY-параметры, запускает RX
раньше TX, содержит один `TransmitGuard.register_transmit()` и cleanup в
`finally`. `--preflight-only` не регистрирует передачу и выполняет только
inventory/common-clock gate с последующим CLKOUT OFF. RF-режим требует два
отдельных CLI-ключа. После отдельной точной авторизации он был выполнен один
раз; повторный запуск не выполнялся и не разрешён.
Фактическая подготовка 1 сентября 2026 года создала внешний каталог
`C:\Users\user\Documents\SDR_Rover_Lab046_raw\20260901_145339_bpsk_object`:
- `tx_object.bin`: 16384 байта, SHA-256
`f7568a5d1a6dc5dd907fa1e971a171578315de740bd6788136f447e409fc6bf8`;
- `tx_stream.cs8`: 313670400 байт, 156835200 complex samples, 19,6044 с,
SHA-256 `8941eb231f1f2f99cde51f3cc5426472dbedf1a61675543e7774faec4b935f5e`;
- `tx_metadata.json`: `LAB046C_PREPARATION_ONLY`, `rf_tx_count = 0`.
Software gate подготовки: quick gate PASS с 482 тестами и 8 функциональными
лабами; полный `pytest` = 482 passed. Безопасный common-clock preflight также PASS:
MASTER reg29 `0x26`, SLAVE CLKIN `DETECTED` после 141,51 мс, SLAVE reg29
`0x26`; после проверки оба CLKOUT выключены, процессов `hackrf_transfer` нет,
`RF TX = 0` на этапе подготовки.
## Lab046C: первый аппаратный результат
Выполнен ровно один непрерывный RF TX всего `tx_stream.cs8`; автоматического
или ручного повтора не было. MASTER/TX
`000000000000000097c063dc323b97d3`, SLAVE/RX
`000000000000000097c063dc2e5327d3`, RF 1080 МГц, Fs 8 MS/s, BPSK 20 ксимв/с,
TX VGA 10 дБ, RX LNA/VGA 24/24 дБ, RF amp и bias OFF. Common clock прошёл
fail-closed gate: MASTER reg29 `0x26`, SLAVE CLKIN `DETECTED`, SLAVE reg29
`0x26`. Передача длилась 19,60632 с и полностью находилась внутри RX capture
длительностью 24,00386 с. USB-журналы чистые: TX underruns 0, RX overruns 0.
Независимый приёмник без TX oracle вернул официальный `RESULT C`:
- `PREAMBLE CANDIDATES = 0`;
- `VALID FRAMES = 0`;
- `PHY HEADER PASS = 0`;
- `CRC PASS = 0`;
- `OBJECT COMPLETE = NO`;
- восстановленный объект не создавался.
Рабочий порог PRBS7 оставался `0.65`; максимальная независимая корреляция
равна `0.126562`. Фоновая медиана мощности `-34.467535 dBFS`, кратковременный
максимум `-34.307678 dBFS`; максимальный средний подъём на интервале 19,6 с
всего `0.012049 dB`. Обычный power/PSD-анализ не показывает заметной передачи.
После завершения и фиксации независимого `RESULT C` выполнена разрешённая
read-only диагностика с TX reference. Она обнаружила следы всех 68 waveform с
точным периодом 2 306 400 samples. Нормированная корреляция в выровненных
позициях: min `0.065784`, median `0.089991`, max `0.122379`; контроль со
смещением: median `0.004275`, max `0.010075`. Медианное преимущество 26,46 дБ,
все 68 aligned значений выше максимума контроля.
Эта post-run диагностика использует знание TX только после официального
решения и **не** превращает опыт в PASS: `OBJECT TRANSPORT PASS` и
`LAB046C COMPLETE SUCCESS` не заявляются. TX-файл, его спектр около +50 кГц,
первый/средний/последний self-decode и runner проверены; дефекта формирования
или управления, объясняющего результат, не найдено. Сохранённый официальный
исход остаётся `LAB046C HARDWARE RESULT = C`, `RF TX COUNT = 1`,
`OBJECT COMPLETE = NO`.
## Lab046C: автономная слабосигнальная обработка исходного RX capture
После аппаратного RESULT C реализован отдельный offline-модуль
`experiments/lab046c_offline_recovery.py`. Lab045 и строгий многокадровый
порог Lab046B `IQ_PREAMBLE_THRESHOLD = 0.65` не изменялись. Новый режим не
содержит HackRF-команд и читает только ранее сохранённый `rx_stream.cs8` через
`np.memmap`.
Первый каскад был зафиксирован до полного прогона по калибровочному фону
`0.0--0.5 s`: порог равен 99,9-му процентилю локальных PRBS7-максимумов.
Получено `threshold = 0.025011632591485977`, correlation median
`0.00838765874505043`, MAD `0.003039387986063957`, 5193 локальных максимума,
6 кандидатов, `12.0 candidates/s`. После фиксации правило проверено на
отдельном контрольном фоне `21.0--24.0 s` и больше не менялось: 35501 локальный
максимум, 301 кандидат, `100.333333 candidates/s`. Временные границы TX
использовались только для выбора участков, где передачи заведомо не было, и не
передавались поиску кадров.
Выполнен ровно один независимый полный offline-прогон 192 000 000 samples без
`tx_stream.cs8`, `tx_object.bin`, TX offsets, ожидаемого числа 68, fragment
order, packets или coded bits. Первый каскад насчитал 291446 локальных
максимумов и 3025 кандидатов выше порога. Все 3025 прошли refinement, timing и
оценку carrier/CFO/phase, но все 3025 были отвергнуты как
`PHY_HEADER_MAGIC_MISMATCH`. `PHY HEADER PASS = 0`, `FEC ATTEMPTS = 0`,
`CRC PASS = 0`, `VALID FRAMES = 0`, `VALID FRAGMENTS = 0`,
`OBJECT COMPLETE = NO`. `recovered_object.bin` не создавался.
Только после завершённого independent verdict была открыта TX reference.
Диагностика показала, что каждый из 68 настоящих стартов имел first-stage
кандидат в пределах `+-40 samples`; корреляция на этих кандидатах
min/median/max `0.059379209/0.088661019/0.126557365`, то есть все они были выше
зафиксированного порога. `FALSE CRC PASS = 0`, но совпавших independently valid
frames также 0, пропущены все 68 TX frames. На известных только post-verdict
позициях локальная физическая экстракция прошла для 68/68, однако ошибки
min/median/max составили `341/430/554` из 1280 бит преамбулы и `30/54/81` из
144 бит PHY header. Оценочный SNR min/median/max
`-19.282130/-12.829480/-9.195846 dB`; все заголовки дали `MAGIC_MISMATCH`.
Следовательно, новый first-stage detector обнаружил слабые следы, но качество
демодуляции оказалось недостаточным даже для защищённого PHY header.
Производительность: calibration `0.269617 s`, control `1.031204 s`, coarse
search `7.721316 s`, strict candidate processing `836.340048 s`, в среднем
`0.276476 s/candidate`, полный прогон `845.427589 s`. Capture длительностью
24 s обработан за `35.226150` его длительности. RX остаётся `memmap`; самый
большой coarse `complex64` block содержит 16 000 000 samples (128 000 000
байт), консервативная оценка временных массивов SciPy `1 545 705 764` байта.
Максимальное strict local window: 594400 samples, 4 755 200 байт. Полный
192M-sample `complex64` массив не создавался.
Артефакты результата находятся в
`data/processed/lab046c_offline_recovery/`: `independent_result.json`,
`post_verdict_diagnostics.json`, `tx_reference_stage_diagnostics.json`.
Правильная итоговая формулировка: **ORIGINAL LAB046C HARDWARE RESULT = C;
OFFLINE REPROCESSING OF ORIGINAL RX CAPTURE = FAIL; OBJECT RECOVERED = NO;
ORIGINAL HARDWARE RESULT CHANGED = NO.** Второй независимый прогон с новым
порогом не выполнялся.
Финальный software gate после результата: `python tools/quick_gate.py` PASS,
506 проверок и 9 функциональных лабораторий; `python -m pytest tests/ -q` =
506 passed. Единственное предупреждение связано с недоступным `.pytest_cache`
и не является отказом. `git diff --check` чист, кроме информационных LF/CRLF
notices.
## Назначение
Lab046 переходит от одиночной 256-байтной полезной нагрузки к передаче
произвольного бинарного объекта последовательностью независимо защищённых
радиокадров. Два уровня строго разделены и не смешиваются:
- **Уровень 1 (внутри одного кадра) — Lab045.** Свёрточный код R=1/2 K=7,
перемежитель, мягкий декодер Витерби, CRC пакета. Исправляет неизвестные
битовые ошибки внутри принятого кадра.
- **Уровень 2 (большой объект) — существующий `protocol/image_fragments`.**
Фрагментация, нумерация фрагментов, идентификатор объекта, сборка. Собирает
объект из нескольких независимо принятых кадров.
Новый код в `experiments/lab046_bpsk_object_transport.py` минимален: он
связывает два готовых уровня и добавляет fail-closed сборщик объекта.
Отдельный модуль формата фрагмента НЕ создавался: переиспользован 12-байтовый
заголовок `image_fragments` (`>IHHI`: object_id u32, fragment_index u16,
fragment_count u16, original_size u32).
## Аудит: что переиспользовано, чего не хватало, минимальный новый слой
1. **Переиспользовано.** `protocol/image_fragments` уже даёт нужную семантику
фрагмента: идентификатор объекта, индекс, число фрагментов, общий размер,
а также fail-closed `reassemble_image` (несовпадение id/count/size,
конфликтующие дубликаты, недостающие фрагменты, финальная проверка длины).
Слой Lab045 даёт кадр целиком: `build_transmission_plan`,
`analyze_post_preamble_soft`, `simulate_soft_channel`, диагностику pre/post-FEC.
2. **Чего не хватало.** Не было (а) упаковки фрагмента в 256-байтную нагрузку
Lab045 с обрезкой добивки на приёме по длине из заголовка и (б) сборщика,
который принимает по одному независимо декодированному кадру и **никогда не
бросает** на плохом входе, возвращая `object_complete = False` вместо
фиктивного объекта.
3. **Минимальный новый слой.** `recover_fragment_from_payload` (fail-closed
декод + обрезка добивки) и класс `ObjectAssembler` (offer/missing/complete/
recovered). Erasure-код 12+3 GF(256) (`protocol/packet_erasure_fec`) НЕ
использован и НЕ изменён — он остаётся будущим расширением для потери целых
фрагментов, а не для битовых ошибок (это работа свёрточного кода).
После этого Lab046A закрыла оставшийся пробел: до неё Lab046 передавала каждый
кадр отдельным вызовом и выбирала его из TX-плана по известному индексу. Функции
поиска нескольких кадров в одном потоке не было. Теперь
`scan_continuous_soft_stream` получает только один массив soft-символов,
самостоятельно ищет PRBS7 после монотонного cursor, проверяет защищённый PHY
header, вычисляет длину coded body из принятого packet length, выполняет готовые
FEC/CRC Lab045 и передаёт принятый fragment в `ObjectAssembler`.
TX offsets, список длин кадров, ожидаемое число 68, порядок/индексы TX,
исходный объект, TX packets и coded bits scanner-у не передаются. TX-side
`frame_starts` остаётся только в генераторе потока и используется тестом для
сравнения уже после независимого receive.
## Граница доказательства Lab046A
Lab046A работает на уровне **одного непрерывного нормализованного soft BPSK
stream** (`float32`). Это разрешённый интерфейс после уже проверенного в Lab045
sample-level front-end: PRBS7 detection, matched filtering, timing, CFO и
phase/polarity recovery. Внутри нового длинного теста эти sample-level операции
заново не моделируются для каждого кадра. Поэтому результат доказывает
самостоятельный поиск границ, header-driven длину, FEC/CRC и сборку объекта в
непрерывном soft stream, но не является аппаратной или непрерывной IQ-проверкой.
Каждый фрагмент содержит полный активный Lab045 PHY frame: 1280 бит PRBS7,
144 бита защищённого header и 4300 coded bits. Точная полная sample-level форма
с guard и хвостом ФРК учитывается как 2 306 400 отсчётов на кадр, однако все
156 835 200 IQ-отсчётов одновременно в памяти не создаются. Это намеренное
ограничение памяти, а не использование известных границ приёмником.
Scanner fail-closed:
- cursor всегда растёт, после принятого кадра переносится за вычисленный конец;
- все PRBS7 candidates выше порога рассматриваются в порядке stream; после
принятого кадра cursor переносится за вычисленный конец и не даёт
декодировать этот кадр повторно;
- плохой header, усечённое body, ошибка FEC/CRC отвергают candidate, но поиск
следующего продолжается;
- `MAX_FRAMES` и `MAX_SEARCH_ITERATIONS` дают явную причину остановки, а не
частичный объект, объявленный полным;
- scan продолжается до конца capture и не останавливается на числе 68.
## Сценарии continuous stream
| Сценарий | Получено |
|---|---|
| Идеальный, кадры подряд | candidates 68, valid headers 68, FEC PASS 68, CRC PASS 68/68, fragments 68, complete=True, SHA совпал, 0 байт-ошибок |
| Фон до/между/после | 37/37/43 soft-символа, valid frames 68, complete=True, SHA совпал |
| Непрерывный AWGN, sigma=0.40, seed=46001 | frames 68, pre-FEC 1791, post-FEC 0, CRC 68/0, complete=True, SHA совпал |
| Valid-looking preamble + bad protected header | candidates 71, rejected 3, последующие valid frames 68, complete=True, SHA совпал |
Фон между кадрами добавлен только после кадров 7, 23 и 51: 11, 19 и 7
soft-символов, всего 37. В baseline дополнительного inter-frame gap нет.
Отдельный тест с валидным header для 12-байтного packet и повреждённым body
проверяет, что scanner берёт длину именно из принятого header, отвергает этот
candidate и затем находит настоящий кадр.
Verifier нашёл опасный вариант прежнего глобального non-maximum suppression:
более сильный ложный PRBS7 peak мог удалить более поздний настоящий старт
кадра. Поэтому suppression убран из списка кандидатов, а добавленный
регрессионный тест проверяет, что ложный peak не скрывает настоящий кадр.
## Lab046B: непрерывный сырой IQ-поток
Lab046B закрывает только программный sample-level пробел Lab046A. Новый
`experiments/lab046b_continuous_iq.py` формирует пять полных Lab045 waveform с
PRBS7, защищённым PHY header, interleaved convolutional body, BPSK, RRC и
guards, размещает их в одном `complex64` массиве и передаёт scanner-у только
этот массив. TX start offsets, длины TX waveform, номера кадров, payload,
packet и coded bits в `scan_continuous_iq` не передаются.
Переиспользованы существующие функции Lab043-Lab045: BPSK/RRC waveform,
PRBS7 reference, normalized sliding correlation, локальное уточнение sample
start, matched filter, preamble-only timing, non-data-aided CFO и комплексный
gain, `normalized.real`, защищённый PHY header, deinterleave, soft Viterbi и
protocol CRC. Новый слой только перечисляет локальные IQ-кандидаты, вызывает
этот тракт на окне кандидата и монотонно продолжает поиск.
Глобальное требование «главный пик значительно выше второго» не используется.
В идеальном пятикадровом опыте peak/second для настоящих кадров лежит около
0 dB, поскольку вторым пиком является другой настоящий кадр; тем не менее все
пять кадров приняты. Все локальные максимумы выше порога сохраняются, а после
CRC PASS cursor переносится за вычисленный по принятому header конец кадра.
После плохого кандидата cursor также строго растёт, но следующий кандидат не
скрывается. Выход ограничен `MAX_FRAMES` и `MAX_SEARCH_ITERATIONS`.
### Фактические сценарии Lab046B
| Сценарий | Получено |
|---|---|
| Идеальный continuous IQ | PRBS7 candidates 32, processed 5, accepted 5, rejected 0, PHY/FEC/CRC 5/5/5, CRC FAIL 0, sample offset error 0 для всех |
| CFO + phase + amplitude | внесено 0.2 Hz / 0.7 rad / 0.65; медианная оценка 0.200003768 Hz / 0.699634288 rad; CRC 5/0 |
| Комплексный AWGN | sigma=0.17, seed=46002; accepted 5, pre-FEC 0, post-FEC 0, CRC 5/0 |
| PRBS7 + неверный PHY header перед настоящим кадром | candidates 11, processed 8, rejected 7, следующий настоящий кадр найден, CRC 1/0 |
Начало первого waveform: sample 137, то есть не sample 0 и не граница сетки
400 samples/symbol. Дополнительные gaps: 211/353/587/809 samples. Найденные
фазы символьной выборки: 137/348/301/88/97 samples modulo 400. Точные TX
offsets используются только постфактум после CRC PASS; ошибки обнаруженного
start во всех пяти случаях равны нулю.
Шумовая точка зафиксирована честно. Первый разумный однокадровый запуск при
sigma=0.12 дал CRC PASS, но 0 pre-FEC ошибок. Выполнена одна разрешённая
обоснованная корректировка до sigma=0.17; она также дала 0 pre-FEC ошибок и
CRC PASS. Дальнейший подбор не выполнялся. Поэтому этот сценарий доказывает
устойчивый многокадровый IQ receive с комплексным детерминированным шумом, но
не является новым доказательством реального исправления ошибок FEC; такое
доказательство уже относится к аппаратной Lab045.
Основной массив содержит 11 534 330 `complex64` отсчётов и занимает
92 274 640 байт (88.0 MiB). Пять waveform занимают 11 532 000 samples, остальное
— leading/gaps/trailing. Полные CFO/phase-corrected копии не создаются:
первичный поиск работает с decimated stream, а matched filtering и soft
extraction выполняются на локальном окне одного кандидата. Это размер именно
основного IQ-массива, а не peak RSS процесса: RRC/FFT, локальный matched stream
и временные массивы SciPy требуют дополнительную рабочую память.
## Память continuous stream
Основной 68-кадровый soft stream содержит 389 232 значения `float32` и занимает
1 556 928 байт (1,485 MiB). Массив correlation scores занимает 1 551 812 байт,
накопленная энергия `float64` — 3 113 864 байта. FFT и denominator создают
временное рабочее пространство, размер которого зависит от SciPy, но стратегия
остается в пределах десятков MiB, а не нескольких GiB. Для сравнения, один
материализованный continuous `complex64` IQ stream из 156 835 200 отсчётов
занял бы около 1,17 GiB ещё до временных копий.
## Детерминированный объект
- Размер **16384 байта (16 KiB)**. Это не JPEG, а воспроизводимый
псевдослучайный поток `numpy.random.default_rng(seed).integers(0,256,...)`.
- **Выбор размера.** 16 KiB — компромисс: достаточно велик, чтобы кадров было
много (68) и учёт фрагментации/сборки был содержательным, но достаточно мал,
чтобы программный прогон 68 кадров с мягким Витерби считался секунды. Ровный
размер (не кратный 244) специально оставляет короткий последний фрагмент.
- Seed объекта по умолчанию `46`, идентификатор объекта `0x4C303436` («L046»).
- SHA-256 объекта по умолчанию:
`748013b5f670c82bd52650be5454555355dbd8528538753df4558f1c3408d515`.
- Объект не сохраняется в Git (крупный генерируемый артефакт); при
необходимости он детерминированно восстанавливается по seed.
## Раскладка нагрузки пакета
Полезная нагрузка Lab045 остаётся 256 байт (доказанная рабочая точка):
[12 байт метаданных фрагмента][244 байта данных фрагмента]
`FRAGMENT_DATA_SIZE = 256 − 12 = 244`. Последний фрагмент короче; его нагрузка
добивается нулями до 256 байт перед Lab045 и обрезается на приёме по длине,
вычисленной **только из полей заголовка** (`expected_fragment_data_length`).
Для 16384 байт последний, 68-й фрагмент несёт 36 байт данных (16384 − 67·244).
## Fail-closed сборка (без оракула)
Приёмник независим: переданный объект и TX-биты не используются ни для приёма
пакета, ни для FEC, ни для восстановления метаданных, ни для решения о
завершении объекта. Сравнение с оригиналом делается только в диагностике,
**после** независимого CRC-решения и независимой сборки.
`ObjectAssembler.offer(fragment)` возвращает `accepted/duplicate/conflicting/
rejected` и никогда не бросает. Отвергаются: чужой object_id, несогласованные
count/original_size, невозможный индекс, конфликтующий дубликат (тот же индекс,
другие данные), malformed/усечённые метаданные. Идентичный дубликат
игнорируется. Пока не собраны все индексы, `object_complete()` = False, а
`recovered_object()` = None — фиктивный объект не создаётся. Поле original_size
проверяется на обе границы (`(count−1)·244 < original_size ≤ count·244`), чтобы
абсурдное значение не управляло размером аллокаций.
## Программные сценарии (seed фиксирован)
| Сценарий | Условие | Ожидалось | Получено |
|---|---|---|---|
| A. Идеальный канал | шума нет | объект точен | complete=True, sha совпал, 0 байт-ошибок, CRC 68/0 |
| B. Распределённые битовые ошибки | гауссов шум sigma=0.40 | pre-FEC>0, post-FEC=0, все CRC PASS, объект точен | pre-FEC 1805, post-FEC 0, CRC 68/0, complete=True, sha совпал |
| C. Приём не по порядку | обратный порядок кадров | объект точен | complete=True, sha совпал |
| D. Дубликаты фрагментов | повтор кадров 0,2,2 | объект точен, дубликаты учтены | duplicates≥1, complete=True, sha совпал |
| E. Один потерянный фрагмент | кадр 1 не дошёл | OBJECT COMPLETE = NO | complete=False, 1 в missing, recovered SHA = None |
| F. Повреждённый пакет | инверсия coded-потока кадра 0 | CRC FAIL, объекта нет | crc_fail≥1, complete=False, recovered SHA = None |
Сценарий B — честная точка: уровень sigma выбран так, чтобы мягкий Витерби
исправлял **все** 68 кадров (при sigma=0.45+ отдельный кадр не декодируется, и
без erasure-FEC объект оказывается неполным — это по сути сценарий E, а не
подгон). Числа не подкручивались ради прохождения: до FEC присутствуют 1805
реальных ошибок кодированных бит, после мягкого Витерби — ноль, объект
побитово точен.
## Метрики (объект 16384 байта, идеальный канал)
| Величина | Значение |
|---|---:|
| Размер объекта | 16384 байта |
| SHA-256 объекта | `748013b5…3408d515` |
| Размер метаданных фрагмента | 12 байт/пакет |
| Размер данных фрагмента | 244 байта (последний 36) |
| Число фрагментов | 68 |
| Размер протокольного пакета | 268 байт (256 нагрузка + 8 header + 4 CRC) |
| Число BPSK-кадров | 68 |
| Всего информационных BPSK-символов | 389 232 (5724 × 68) |
| Всего отсчётов формы сигнала | 156 835 200 (2 306 400 × 68) |
| Baseline: дополнительный inter-frame gap | 0 отсчётов |
| Baseline: полная длительность | 19,604400 с |
| Baseline: полезный goodput | 835,7 байт/с = 6,685846 кбит/с |
| Фоновый тест: leading/inter-frame/trailing | 14 800 / 14 800 / 17 200 отсчётов |
| Фоновый тест: полная длина и длительность | 156 882 000 отсчётов, 19,610250 с |
| Фоновый тест: фактический goodput | 6,683852 кбит/с |
| Полезных бит на символ | ≈ 0,337 |
Учёт кадра совпадает с Lab045: 2144 информационных бита, 6 tail-бит, 4300
кодированных бит (R=1/2), перемежитель 4300, PHY header 48→144 бит (×3),
преамбула PRBS7 1280 бит, итого 5724 информационных символа кадра. Форма
сигнала добавляет 42 символо-времени (32 нулевых guard + 10 хвост ФРК,
`shape_frame_like_lab042`, режим `full`): 5766 символо-времён = 2 306 400
отсчётов = 288,3 мс на кадр при 8 MS/s.
### Разбор накладных расходов
- **Фрагментация (уровень 2):** 12 байт метаданных на пакет × 68 = 816 байт.
- **Протокольный header+CRC (уровень 1):** 12 байт на пакет × 68 = 816 байт.
- **Свёрточный код R=1/2:** удваивает число кодированных бит (2144→4300 с tail).
- **PHY преамбула и защищённый header:** 1280 + 144 = 1424 бита на кадр.
- **Guard/фильтр:** 42 символо-времени на кадр (32 guard + 10 хвост ФРК).
- **Добивка последнего фрагмента:** 208 нулевых байт (только 68-й кадр).
Goodput 6,69 кбит/с — это честная baseline при 20 ксимв/с с полной
преамбулой и guard на каждый кадр; в реальной серии кадров преамбула на кадр
доминирует в накладных расходах, и это направление для будущей оптимизации,
а не дефект.
## Фактические программные результаты
- Новый набор `tests/test_lab046_bpsk_object_transport.py`: все проверки
проходят (фрагмент туда-обратно, короткий последний фрагмент, чужой
object_id, невозможный индекс, несогласованный счётчик, усечённые и абсурдные
метаданные без крупной аллокации, полный тракт PHY для сценариев A–F,
отсутствие оракула, последовательный continuous scan, rejected candidate,
MAX_FRAMES/MAX_SEARCH_ITERATIONS, регрессия против удаления настоящего кадра
ложным PRBS7 peak, детерминизм и SHA объекта).
- Полный тракт `объект → фрагменты → кадр Lab045 → канал → приём → сборка`
проверен для всех шести сценариев значениями выше.
## Software gate Lab046B на момент его фиксации
- `python tools/quick_gate.py`: PASS; 468 тестов ядра, 7 функциональных
проверок лабораторий выполнено, 13 штатно пропущено.
- `python -m pytest tests/ -q`: 468 passed, 0 failed, 0 errors, 0 skipped.
Единственное предупреждение связано с отсутствием прав на создание
`.pytest_cache`; на выполнение и результат тестов оно не повлияло.
- `git diff --check`: чист.
Актуальный общий gate после добавления Lab046C указан в начале документа.
## Ограничения и следующий шаг
Для Lab046A/B RF TX = 0; первый аппаратный Lab046C завершён с RESULT C и одним
RF TX без повтора. Erasure-код 12+3 GF(256) не задействован. Порог PRBS7,
gain, PHY, FEC и detector после результата не менялись. Следующий этап
определяется отдельной постановкой. Offline weak-signal прогон с заранее
зафиксированным правилом уже завершён как FAIL и не повторяется с другим
порогом. Новый RF-сеанс автоматически не разрешён.