Контракты пилота П-3 (air-nn)

Состояние на 04.10.2026: П-3 закрыт, кодировка П2 v5 в игру не принята (в игре сеть P2 прежней кодировки); контракт сохранён как справка к архиву опыта.

План — docs/archive/plan/air-nn-p3.md; прежние версии П2 (v1–v4) и П3 (v1–v3) — docs/contracts/air-nn.md (там же — П1, П6, их форма не меняется). Код — tools/research/air_nn_pilot/pilotnn/. Зафиксировано координатором 03.10 по коду prep.py (П2 v4) и evaluate.py (П3 v3) до запуска исполнителей. Изменение — только через координатора.

Задачи (журнал dp): П3-А = NN-P9, П3-Б = NN-P10, П3-В = NN-P11, П3-Г = NN-P12, П3-Д = NN-P13.

Б1. Линейная база обтекания (версия 1)

Владелец: NN-P9 (pilotnn/base.py). Потребители: NN-P11 (карты отрыва от склона используют hs_grad), NN-P10 (формы профиля в кодировке v5), NN-P12 (вход 24–26, выход v5), в будущем air-onnx (O3 v2 — перенос в GDScript).

linear_base(hc_r, r, ub, agl=AGL, dx=400.0) -> dict(
    u, v, w,      # (13, ny, nx) float64, м/с, повёрнутая система (u — по x′, v — по y′), поле базы V_base, w_base
    gx, gy)       # (13, ny, nx) float64, м/м: ∇h_s(z_a) — уклон рельефа, сглаженного на масштабе высоты z_a
  • Вход: hc_r — повёрнутый рельеф [j′, i′] (м, rot_scalar(d400_hc, k)), r — остаток угла (рад, ê′ = (cos r, sin r)), ub — профиль притока Ub(z_a) (13, м/с, prep.ubg), agl — высоты над рельефом (м).
  • h_s(z) = F⁻¹[ĥ(k)·exp(−‖k‖·z)] от hc_r − mean (затухание неровности масштаба L как e^(−z/L) — потенциальное течение); ∇ — спектрально или центральными разностями (выбор NN-P9, с записью). Продолжение до периодичности — чётное отражение до 2N × 2N (по умолчанию; NN-P9 может сменить один раз с обоснованием, запись здесь).
  • Поле базы на высоте z_a — линейная теория обтекания нейтрального потока со скоростью Ub(z_a) вдоль ê′ (по умолчанию потенциальное течение; поправка Jackson–Hunt внутреннего слоя — на выбор NN-P9 по таблице «база против решателя», с записью): w_base = Ub·(ê′·∇h_s), (u, v)_base = Ub·ê′ + возмущение. Знаки — по тесту: на вершине изолированного холма разгон > 0, на наветренном склоне w > 0, на подветренном < 0.
  • Инварианты (тест NN-P9 tests/test_base.py; форма — контрактный тест tests/test_contract_p2v5.py): аналитический холм (Аньези 2D и гаусс 3D) — против известного линейного решения; повороты ×4 — равновариантность (база повёрнутого рельефа = повёрнутая база); отражение y′ → −y′ с r → −r даёт отражённую базу (v → −v); плоский рельеф → (Ub·ê′, 0); побитный повтор. Время на случай 96² × 13 на CPU — в отчёте (ориентир — десятки мс).
  • Граница у края (ревью NN-P9): при чётном отражении производная на плоскости зеркала → 0 — gx, gy, w_base в крайнем ряду клеток занижены; оценка исключает край (edge_cells), цель обучения — нет (обратимость не страдает).
  • Где нет решения: нет (база определена всюду). Ub = 0 (U10 = 0) → база нулевая; кодировка выхода это учитывает (ниже).

П2. Вход и выход сети области пилота (версия 5)

Владелец: NN-P12 (сборка подготовки, обучения, оценки и экспорта), карты — NN-P11, база и кодировка выхода — NN-P9. Потребители: оценка и отчёт (NN-P12, NN-P13), экспорт ONNX → air-onnx (O1, O3 — только если вариант пойдёт в игру, см. «Что меняется для air-onnx»).

Кодировка выбирается конфигом прогона: encoding: {inputs: v4|v5, outputs: v4|v5, pack: 0|K}; v4 — как П2 v4 без изменений (9 карт, 91 канал). Повороты, числа FiLM (18, prep.FILM_NAMES), деление, отражение как аугментация — как v4. Новое — только ниже.

Вход v5 — 27 карт (float32, 96 × 96, повёрнутая система, фиксированные нормировки)

Первые 9 — ровно карты v4 (prep.MAP_NAMES, те же формулы и нормировки): вход v4 = первые 9 карт v5. (В плане «24 карты» — описка суммы групп: 7 + 8 + 5 + 4 + 3 = 27; уклон 400 м = v4 slope_along/cross.)

#имяформуланорм.знак при R
0–8terrain … shelterкак П2 v3/v4как v4как v4 (y, slope_cross — минус)
9, 10slope_along_1k2, slope_cross_1k2∇(G_σ hc′)·ê′, ·ê′⊥, σ = 600 м/0,3+, −
11, 12slope_along_3k2, slope_cross_3k2то же, σ = 1600 м/0,3+, −
13, 14slope_along_8k, slope_cross_8kто же, σ = 4000 м/0,3+, −
15sub_slope_stdstd ‖∇h₂₅‖ по подклеткам 25 м клетки 400 м/0,3+
16sub_slope_p9595-й процентиль ‖∇h₂₅‖ в клетке/0,6+
17sub_steep_leeдоля подклеток с ∇h₂₅·ê′ < −0,3 (круто вниз по ветру)—+
18sub_steep_windдоля подклеток с ∇h₂₅·ê′ > +0,3 (круто вверх по ветру)—+
19sub_reliefmax − min h₂₅ в клетке/300 м+
20–22sep_400, sep_1k2, sep_3k2σ((−s_L − 0,3)/0,05), s_L — уклон вдоль ветра масштаба L (м/м, карты 4, 9, 11 без нормировки)—+
23sep_wakemax по клеткам q выше по ветру: sep_400(q)·exp(−d/(k_w·H(q))), d — расстояние по ветру от q, H(q) — перепад за кромкой q, k_w = 7—+
24–26base_a25, base_a150, base_a600ln(max(‖V_base‖, ε)/max(Ub, ε)) на 25, 150, 600 м (Б1), ε = 0,1 м/с—+
  • G_σ — gaussian_filter(hc′, σ/400 м, mode="nearest"), ∇ — np.gradient(·, 400 м), как v3.
  • h₂₅ — рельеф 25 м (tiles/v3) места, в повёрнутой системе (поворот подклеток вместе с клеткой); подклетки клетки [j′, i′] — 16 × 16. Нет данных 25 м (процедурные p_*, синтетика, вне тайла) — h₂₅ = билинейная интерполяция d400_hc (те же формулы; подсеточного рельефа нет) и счётчик таких случаев в отчёте NN-P11. Так же поступит игра без подробного рельефа.
  • Константы карт 9–23 (σ, пороги 0,3 и ширина 0,05, k_w, определение H, нормировки) владелец NN-P11 может изменить один раз до опытов NN-P12 с обоснованием (таблица NN-P11) — запись здесь, версия не меняется.
  • Итог NN-P11 (03.10): константы карт 9–23 — как в таблице (варианты порогов 0,2/0,3/0,45, k_w 3,5/7/14 различаются по R² ≤ 0,002); H(q) — перепад от кромки q до минимума высоты в 4 км вниз по ветру, не меньше 25 м; подклетки — блок 16 × 16 узлов тайла 25 м (как terrain_cut.block_mean_400), узлы 31…1568 тайла 1601².
  • Знаки при отражении R (y′ → −y′): все новые карты — только разворот по j, кроме slope_cross_* (минус).
  • Код: pilotnn/maps5.py — MAP_NAMES_V5 (27 имён, порядок таблицы), REFLECT_MAP_SIGN_V5, maps_v5(z, meta, row, tile=None) -> (27, ny, nx) float32; карты 24–26 берут linear_base (Б1).

Выход v5 — 9 величин × 13 высот = 117 каналов (канал = c·13 + a, повёрнутая система)

cвеличинаформулазнак при R
0 / 4разгон к базе a (без / с нагревом)ln(max(‖V‖, ε)/max(‖V_base‖, ε)), ε = 0,1 м/с, V — горизонтальный ветер решателя+
1 / 5sin δδ = угол(V) − угол(V_base); при ‖V_base‖ < ε угол базы = r−
2 / 6cos δ+
3 / 7отрыв от склона w_rel(w − γ_a·V·∇h_s(z_a)) / S, S = max(U10, 1 м/с), ∇h_s — Б1 gx, gy, γ_a — ниже+
8θ′ (с нагревом)К, без масштаба+
  • Код (NN-P9, pilotnn/base.py): OUT_NAMES_V5 (9 имён: a_m, sd_m, cd_m, wrel_m, a_h, sd_h, cd_h, wrel_h, theta), REFLECT_OUT_SIGN_V5 (117), target_v5(z, meta, base, agl=AGL) -> (117, ny, nx) float64, to_physical_v5(y, meta, base, agl=AGL) -> dict(m, h); base — итог linear_base случая. NN-P12 сводит v4/v5 в prep.to_physical(y, meta, agl, enc=…, base=…).
  • γ_a (уточнение v5 от 03.10, до опытов; NN-P9: w решателя на 400 м в 3–4 раза меньше кинематики V·∇h_s, регрессия 0,27–0,32 на 60 м, rms w_rel 0,47 > rms w 0,29 м/с): коэффициент по высоте (13, отдельно m и h) — наименьшие квадраты w на V·∇h_s по обучающим случаям, считает NN-P12, хранится в enc.json прогона (и в метаданных ONNX); γ = 1 — чистая кинематика (так написан base.target_v5 NN-P9 — параметр gamma добавляет NN-P12), γ = 0 — цель w. Обратное: w = w_rel·S + γ_a·V·∇h_s(z_a).
  • Обратное преобразование (to_physical_v5; для v4 — прежняя prep.to_physical): ‖V‖ = max(‖V_base‖, ε)·eᵃ, угол = угол(V_base) + atan2(sin δ, cos δ) (вектор (sin δ, cos δ) не нормируется заранее — atan2 сам), w = w_rel·S + γ_a·V·∇h_s(z_a), θ′ как есть; затем обратный поворот, как v4. Итог — dict(m, h) в м/с и К исходной системы — тот же формат, что v4; ею пользуются оценка, отчёт и проверка ONNX.
  • Тест: цель → to_physical = поле решателя (с точностью float16 цели; ‖V‖ < ε — ошибка ≤ ε); повороты ×4; R∘R = тождество; цель отражённого образца = R(цели).
  • Ошибка обучения (не интерфейс, выбор NN-P12 с записью в README прогона): разгон и поворот — с весом скорости (≈ ошибка вектора), поворот не считается при ‖V‖ < 0,5 м/с; w_rel — свой вес.
  • Упаковка по высоте (pack: K, только если NN-P10 дал «да»): q(z_a) ≈ μ(z) + Σₖ cₖ·φₖ(z), φ — формы NN-P10 по обучению (файл pod.npz прогона: mean [9, 13], phi [9, K, 13], float32). Сеть предсказывает 9·K коэффициентов, распаковка — неподвижный линейный слой внутри сети: выход сети и ONNX — всегда 117 каналов.

Что меняется для air-onnx (не делаем, пока вариант не идёт в игру)

  • O1 v2: maps [1, 27, 96, 96] (или 9 — вход v4), out [1, 117, 96, 96] (или 91); метаданные deltaplan.p2_version = “5”, + deltaplan.out_enc (“v4” | “v5”); nums — без изменений.
  • O3 v2: в AirNnPrep — линейная база Б1 (FFT 192² на CPU, 13 высот), карты 9–26 (нужен рельеф 25 м вокруг области или билинейная подстановка), to_physical v5 с базой.
  • Тест tools/air_onnx/test_contract_o1.py (ветка air-onnx) на v5 не пройдёт без O1 v2 — ожидаемо; NN-P13 проверяет форму по этому разделу и запускает O1-тест на варианте с кодировкой v4 (если такой лучший).

П3. Отчёт пилота (версия 4)

Владелец: NN-P12 (таблица вариантов), NN-P13 (сводка заменимости, ШП-3). Потребители: пользователь (ШП-3), координатор. Числа v3 (evaluate.py/report.py: (г), (б), (б′), (а), сошедшиеся/несошедшиеся, смещение, кривая) — без изменений; добавляется:

  • Таблица вариантов $AIR_NN_DATA/pilot/reports/<дата>_p3/variants.md + variants.json, по строке на вариант (0)…(5) и лучший на 300 местах, рядом П-2 (2026-10-03_p2b, main и curve_100): на (г) сошедшиеся — медиана и p90 ошибки ветра на 60 м (все клетки области), «ок» ветра и подъёма (без/с нагревом), смещение e (среднее, ≤ 300 м), заменимость (ниже); на (б) — медиана ветра; время эпохи, размер и время ONNX (4 потока). Контроль (0) — переобучение П-2 на 100 местах тем же скриптом; разница с curve_100 П-2 — оценка разброса от зерна (в таблице).
  • Заменимость (решение пользователя 03.10: отличие сети от решателя заметно меньше отличия решателя от реальности). Погрешность решателя в точке: u_ref = max(0,3 м/с; 0,15·‖V_решателя‖) (середина Askervein ±10–20 %; пол — как у «ок»), у несошедшихся — max(u_ref, late_spread60_p90). Отношение ρ = ошибка ветра сети / u на 60 м. В таблице: медиана и p90 ρ по клеткам; по группам (сошедшиеся, несошедшиеся, (г) по горным системам). «Заменима»: на (г) сошедшиеся медиана ρ ≤ 0,5 и p90 ρ ≤ 1,0 (пороги — config.yaml; правило — решение координатора, на ШП-3 пользователь подтверждает или меняет).
  • Вердикт ШП-3 (правилом, по таблице): «кодировка» — лучший из (2)–(5) на (г) лучше и (0), и (1) по медиане ветра на ≥ 10 % и не хуже по p90 → берём кодировку П-3 в волну 0 / air-onnx; «ёмкость» — (1) лучше лучшего из (2)–(5) на ≥ 10 % → сеть шире и каскад; «данные» — ни один вариант не лучше (0) на ≥ 10 % → массовый счёт. Отдельной строкой — «заменима: да/нет» лучшего варианта на 300 местах.