Водопад¶
src/soniks_client/waterfall/ делает две разные вещи: рисует PNG-водопад для
портала и проводит самостоятельный анализ сигнала, результат которого уезжает
как metadata["signal"].
Пакет разложен по слоям — каждый следующий зависит только от предыдущего:
Модуль |
Что внутри |
Зависимости |
|---|---|---|
|
12 приватных примитивов numpy: обнаружение всплесков, поиск пиков, полосы, k-means-2 |
только |
|
|
|
|
класс |
|
|
реэкспорт |
— |
Всё, кроме класса Waterfall, приватное: имена начинаются с _ и наружу не
выдаются.
Примечание
До фазы 3 это был один файл на 2214 строк, в котором около 900 строк не
вызывались ниоткуда — остатки прежних поколений оценщика
(_estimate_deviation_fft_only, _compute_collision_doppler_curve и их
хелперы). Они удалены; оценщик теперь один, и запасных веток у него нет. Если
имя из старых обсуждений не находится — ищите его в истории git, а не в коде.
Публичный интерфейс¶
wf = Waterfall(datafile_path, output_path) # читает .dat, может бросить исключение
wf.plot(vmin=None, vmax=None) # обе границы необязательны
signal = wf.get_signal_metadata() # dict | None
Конструктор сразу читает файл и проверяет, что данные есть. Три исключения:
Исключение |
Когда |
|---|---|
|
Файл не найден, это не файл, заголовок обрезан или не читается |
|
Файл прочитан, но массив отсчётов пуст |
|
Метка времени в заголовке не разбирается |
Вызывающий код — post_processing.build_waterfall() — ловит все три и продолжает
выгрузку без водопада: отсутствие картинки не повод терять аудио и кадры. Сырой
.dat при этом остаётся на диске — он удаляется только после успешного
plot(), чтобы упавшее построение можно было воспроизвести.
get_signal_metadata(), вызванный до plot(), возвращает None: анализ
наполняется в ходе отрисовки.
Формат файла¶
Потоковый граф пишет сырой водопад в бинарный .dat с заголовком
фиксированного размера, затем строками спектра:
Поле |
Тип |
Смысл |
|---|---|---|
|
|
Метка времени начала, ASCII |
|
|
Число частотных каналов (ширина БПФ) |
|
|
Частота дискретизации |
|
|
Сколько БПФ усреднено в одну строку |
|
|
Центральная частота |
|
|
Порядок байтов остальных данных |
Далее — записи (tabs: i8, spec: f4[nchan]): относительная метка времени и
строка спектра. Порядок байтов определяется полем endianness, поле
center_freq при этом всегда читается big-endian — так пишет граф.
Из этого восстанавливаются ось времени (trel) и ось частот (freq,
симметричная относительно нуля в диапазоне ±samp_rate/2).
Файл крупный и по умолчанию удаляется после отрисовки
(OBSERVATION__REMOVE_WATERFALL_RAW_FILES=True). Для отладки алгоритмов
выключите удаление — иначе воспроизвести обработку будет не на чем.
Отрисовка¶
plot() рисует PNG размером WATERFALL__WIDTH × WATERFALL__HEIGHT дюймов с
раскладкой из WATERFALL__GRIDSPEC (полотно плюс цветовая шкала).
Границы динамического диапазона подбираются, только если хотя бы одна из них не
передана в plot(). Штатный вызов из post_processing() идёт без аргументов,
поэтому настройки ниже действительно работают:
WATERFALL__AUTORANGE=False(по умолчанию) — фиксированныеWATERFALL__MIN_VALUE/MAX_VALUE, по умолчанию −110 и −50 дБ;AUTORANGE=True— робастная оценка уровня шума по данным (сигма-клиппинг). Отсчёты фильтруютсяnp.isfiniteи порогомWATERFALL__THRESHOLD(−200 дБ):isfiniteубирает-infпустых бинов, порог — заведомо нефизические уровни. Если после фильтрации осталось меньшеWATERFALL__MIN_VALID_SAMPLES(10000) отсчётов, берутсяDEFAULT_MIN_VALUE/DEFAULT_MAX_VALUE, а в лог уходитwarning.
WATERFALL__HELP_LINES=True добавляет поверх водопада вспомогательную сетку.
Анализ сигнала¶
get_signal_metadata() форматирует накопленный в self.analysis результат в
плоский словарь под схему портала. Ключи со значением None выбрасываются,
поэтому состав ответа зависит от того, что удалось измерить. Если анализа не
было (plot() ещё не вызывался), метод возвращает None целиком.
Группы измерений:
Группа |
Ключи |
|---|---|
Отношение сигнал/шум |
|
Модуляция |
|
Девиация |
|
Полоса |
|
Активность |
|
Частота |
|
Прочее |
|
Геометрия водопада |
|
Величины с размерностью — строки с единицами измерения ("1234.5 Hz",
"-98.3 dB", "437.050000 MHz", "1.234 ppm"), так их ожидает портал; при
изменении формата сломается отображение на стороне сайта. Счётчики
n_spec_peaks, n_bursts, n_valid_rows уходят числами, saturated —
bool (опора цели упёрлась в край полосы: сигнал шире водопада или приёмник
перегружен; порталу — считать долю таких проходов, Фаза 4), а modulation,
deviation_method и method — текстом как есть.
Примечание
Ключ bw99_hz в ответе портала читается из bw_99_hz словаря оценщика.
Одноимённый bw99_hz внутри самого оценщика — legacy: ему никто не присваивает
значения, и он всегда None.
Геометрия берётся из заголовка .dat, а не из оценщика, поэтому есть даже там,
где анализ не состоялся. Она нужна порталу: полоса водопада равна out_samp_rate
графа и зависит от режима (48 кГц у FM, 57.6 кГц у FSK 9600, 76.8 кГц у BPSK,
66.56 кГц у APT/SSTV, 200 кГц у PHASMA) при постоянном БПФ на 1024 точки, так
что noise_db — мощность в бине — между режимами несравним. noise_dbhz =
noise_db − 10·lg(bin_hz) — плотность шума, сопоставимая между режимами и
станциями с разной геометрией (Фаза 3.3 в дорожной карте сети).
Формула точна для белого шума при прямоугольном окне и масштабе 1/fft_size
waterfall_sink; смещение режима Max Hold (10·lg(H_n) по nfft_per_row,
от 3.2 дБ при 4 БПФ на строку до 5.5 дБ при 19) не вычитается — режим sink в
заголовке не записан.
Практическая польза: ppm_error и carrier_offset_hz показывают уход опорного
генератора вашего приёмника. Систематическое смещение из наблюдений — готовое
значение для FLOWGRAPH__PPM_ERROR.
Валидация¶
Оценщик проверяется на синтетических спектрограммах в tests/test_deviation.py.
Генератор _synthetic_spectrum() повторяет то, что пишет waterfall_sink:
мощность комплексного гауссова шума в бине распределена экспоненциально, Max
Hold берёт максимум по nfft_per_row БПФ, итог в дБ; тона — гауссовы лепестки
в линейной мощности. Отсюда проверяемое смещение полки Max Hold — 10·lg(H_n),
4.34 дБ при 8 БПФ на строку.
Каждый тест закрепляет одну ветку конвейера из шапки deviation.py: k-means по
пикам строк (тона ±Δf через строку), пара пиков усреднённого спектра (оба
тона в каждой строке, один сильнее), σ_RMS для одного лепестка, CW, weak/noise,
смещение несущей и ppm, изоляция помехи, насыщение, guard на малом водопаде —
и ключевое утверждение Фазы 3.3: один N0 при разной ширине бина даёт один
noise_dbhz.
Потолки, которые тесты закрепляют как есть, а не чинят:
interferer_rejectedзначит «оценён сигнал не по центру», а не «помеха отброшена»: слабая далёкая помеха отбрасывается молча с флагомFalse, сильная — измеряется с флагомTrue;непрерывный сигнал всегда классифицируется как
CW Morse (bursty …): при всех горячих строках_detect_burst_rowsнаходит меньше пяти, фолбэк по 75-му перцентилю даётbursty_fraction0.25, веткаCW continuousнедостижима;критерий насыщения
bw99 > 0.85·samp_rateнедостижим из-за среза ±0.45 (шапка обещает/2);saturatedловит только касание края опорой цели, а широкий центрированный сигнал уходит вweak/noise— робастный шум берёт медиану самого сигнала;legacy-ключи словаря оценщика (
bw99_hz,bw95_hz,w30_*,delta_f_*_hz) никем не присваиваются.
Работа с модулем¶
Правка алгоритма — это
deviation.py; правка отрисовки —plot.py. Общие численные примитивы вdsp.pyтрогайте в последнюю очередь: их использует оценщик в десятке мест.Аннотации типов здесь почти отсутствуют (в отличие от остального кода), и autodoc покажет голые сигнатуры. Это известное состояние.
Приватные хелперы в Справочник API не выводятся — документируются только
Waterfall,plot()иget_signal_metadata().Для воспроизводимой отладки сохраняйте
.dat-файлы:OBSERVATION__REMOVE_WATERFALL_RAW_FILES=False.