Контракты пилота П-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–8 | terrain … shelter | как П2 v3/v4 | как v4 | как v4 (y, slope_cross — минус) |
| 9, 10 | slope_along_1k2, slope_cross_1k2 | ∇(G_σ hc′)·ê′, ·ê′⊥, σ = 600 м | /0,3 | +, − |
| 11, 12 | slope_along_3k2, slope_cross_3k2 | то же, σ = 1600 м | /0,3 | +, − |
| 13, 14 | slope_along_8k, slope_cross_8k | то же, σ = 4000 м | /0,3 | +, − |
| 15 | sub_slope_std | std ‖∇h₂₅‖ по подклеткам 25 м клетки 400 м | /0,3 | + |
| 16 | sub_slope_p95 | 95-й процентиль ‖∇h₂₅‖ в клетке | /0,6 | + |
| 17 | sub_steep_lee | доля подклеток с ∇h₂₅·ê′ < −0,3 (круто вниз по ветру) | — | + |
| 18 | sub_steep_wind | доля подклеток с ∇h₂₅·ê′ > +0,3 (круто вверх по ветру) | — | + |
| 19 | sub_relief | max − min h₂₅ в клетке | /300 м | + |
| 20–22 | sep_400, sep_1k2, sep_3k2 | σ((−s_L − 0,3)/0,05), s_L — уклон вдоль ветра масштаба L (м/м, карты 4, 9, 11 без нормировки) | — | + |
| 23 | sep_wake | max по клеткам q выше по ветру: sep_400(q)·exp(−d/(k_w·H(q))), d — расстояние по ветру от q, H(q) — перепад за кромкой q, k_w = 7 | — | + |
| 24–26 | base_a25, base_a150, base_a600 | ln(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 / 5 | sin δ | δ = угол(V) − угол(V_base); при ‖V_base‖ < ε угол базы = r | − |
| 2 / 6 | cos δ | + | |
| 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_v5NN-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_physicalv5 с базой. - Тест
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 местах.