# Водопад `src/soniks_client/waterfall/` делает две разные вещи: рисует PNG-водопад для портала и проводит самостоятельный анализ сигнала, результат которого уезжает как `metadata["signal"]`. Пакет разложен по слоям — каждый следующий зависит только от предыдущего: | Модуль | Что внутри | Зависимости | |---|---|---| | `dsp.py` | 12 приватных примитивов numpy: обнаружение всплесков, поиск пиков, полосы, k-means-2 | только `numpy` | | `deviation.py` | `_estimate_deviation_v7` — единственный оценщик девиации, вызывает примитивы из `dsp` | `numpy`, `dsp` | | `plot.py` | класс `Waterfall`: чтение `.dat`, отрисовка, метаданные. Весь matplotlib | `deviation`, `core` | | `__init__.py` | реэкспорт `Waterfall` — импорт снаружи остаётся `from soniks_client.waterfall import Waterfall` | — | Всё, кроме класса `Waterfall`, приватное: имена начинаются с `_` и наружу не выдаются. ```{note} До фазы 3 это был один файл на 2214 строк, в котором **около 900 строк не вызывались ниоткуда** — остатки прежних поколений оценщика (`_estimate_deviation_fft_only`, `_compute_collision_doppler_curve` и их хелперы). Они удалены; оценщик теперь один, и запасных веток у него нет. Если имя из старых обсуждений не находится — ищите его в истории git, а не в коде. ``` ## Публичный интерфейс ```python wf = Waterfall(datafile_path, output_path) # читает .dat, может бросить исключение wf.plot(vmin=None, vmax=None) # обе границы необязательны signal = wf.get_signal_metadata() # dict | None ``` Конструктор сразу читает файл и проверяет, что данные есть. Три исключения: | Исключение | Когда | |---|---| | `WaterfallFileError` | Файл не найден, это не файл, заголовок обрезан или не читается | | `EmptyWaterfallError` | Файл прочитан, но массив отсчётов пуст | | `TimestampError` | Метка времени в заголовке не разбирается | Вызывающий код — `post_processing.build_waterfall()` — ловит все три и продолжает выгрузку без водопада: отсутствие картинки не повод терять аудио и кадры. Сырой `.dat` при этом **остаётся на диске** — он удаляется только после успешного `plot()`, чтобы упавшее построение можно было воспроизвести. `get_signal_metadata()`, вызванный **до** `plot()`, возвращает `None`: анализ наполняется в ходе отрисовки. ## Формат файла Потоковый граф пишет сырой водопад в бинарный `.dat` с заголовком фиксированного размера, затем строками спектра: | Поле | Тип | Смысл | |---|---|---| | `timestamp` | `S32` | Метка времени начала, ASCII | | `nchan` | `>i4` | Число частотных каналов (ширина БПФ) | | `samp_rate` | `>i4` | Частота дискретизации | | `nfft_per_row` | `>i4` | Сколько БПФ усреднено в одну строку | | `center_freq` | `>f4` | Центральная частота | | `endianness` | ` 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](../api/index.md) не выводятся — документируются только `Waterfall`, `plot()` и `get_signal_metadata()`. * Для воспроизводимой отладки сохраняйте `.dat`-файлы: `OBSERVATION__REMOVE_WATERFALL_RAW_FILES=False`.