Контракты модуля air-nn
План — docs/plan/air_nn.md, журнал — docs/archive/plan/air-nn-progress.md. Контракты волны 0 и далее (N1–N6, §8.1
плана) будут добавлены перед своими задачами. Ниже — только стыки этапа П (пилот); код пилота —
исследовательский (tools/research/air_nn_pilot/), контракты пилота не переходят в N1–N4 автоматически.
Изменение интерфейса — только через координатора: версия +1, что изменилось, уведомление потребителей.
П1. Образец набора пилота (версия 3)
Владелец: NN-P1 (генератор). Потребители: NN-P2 (загрузчик, обучение, оценка), NN-P3 (проверка).
- Каталог набора:
$AIR_NN_DATA/pilot/datasets/<версия решателя>/<имя набора>/;<версия решателя>—s0-<7 знаков хеша>от кодаtools/research/air3d/*.py(решатель не правится);manifest.json,plan.json(план условий с зерном),state.sqlite(статусы — §4.5 плана:cases,events,meta),cases/<id>.npz.AIR_NN_DATA— из окружения, по умолчанию~/air_nn_data(через конфиг, не зашит в код). - Случай = место × условия (час, U10, откуда дует, t_max, облачность); два решения эталона AM-01 («как игра»):
с нагревом (
h) и без (m). Область 400 м, 96 × 96, центр места в (0, 0); окна 100 м 64 × 64 у стартов. cases/<id>.npz, все массивы float16, раскладка air-lite (research/air-lite7cc7e33,gen.py) без изменений:ключ форма что d400_h(4, 13, 96, 96) u, v, w, θ′ с нагревом d400_m(3, 13, 96, 96) u, v, w без нагрева (механика) d400_hc(96, 96) высота рельефа клетки, м над морем d400_H(96, 96) поток явного тепла в решатель, Вт/м² (вход, как AirPlace.solar_flux)d400_hbl(96, 96) толщина слоя решателя, м w<k>_h,w<k>_m,w<k>_hc,w<k>_H,w<k>_hblто же для окна k, 64 × 64 окна 100 м (пилот их не обучает) Оси: [канал, высота, j, i], j — север (y), i — восток (x); u — на восток, v — на север, w — вверх, м/с;θ′ — отклонение от фона, К. Высоты над рельефом клетки (AGL): 25, 50, 75, 100, 150, 200, 300, 400, 600, 800, 1100, 1500, 2000 м ( plan.json → agl). Значения конечные (NaN нет — проверяется тестом).- Метаданные случая (условия,
day,profile= α, max_profile, класс устойчивости, высота/азимут солнца, сетка уровнейd400/w<k>: dx, dz, x0, y0, nx, ny, nz, z_bot, статусы и итерации решений) — строкаcasesвstate.sqlite(JSON), как строкаruns.jsonlair-lite. Для анализа — выгрузка скриптом. - Места: 4 встроенных (
ongudai,aushkul,altai,askarovo), 5 синтетик air-lite (s_*) и процедурные рельефыp_<NNN>(зерно в плане). Координаты мест: x — восток, y — север, м от центра. - Инварианты: файл появляется только целиком (временное имя → fsync → переименование), статус
done— только после файла; повтор случая даёт побитно тот же файл. - v2 (02.10, этап П-2): + набор
terrain(имя вconfigs/dataset.yaml) — только область: вcases/<id>.npzключиd400_h,d400_m,d400_hc,d400_H,d400_hbl(формы, dtype, оси — как выше), оконw<k>_*нет;manifest.json/meta.contract=П1 v2. Места —t_<NNNN>из индекса П6 (пул и отложенные системы, все строки индекса) + при желании прежние места; условия —n_condна место (10–12, конфиг), распределение и стратификация по часу × облачности — какplan_rows(каждый 12-й — штиль), своё зерно.plan.json → centers[t_*]— 3 точки наибольшего превышения над окрестностью 2 км (window_centers), только для оценки (окна по ним не считаются). В метаданных случая — +ctx(lat, lon, month, day, utc_offset_h, долина) иplace(system,partиз П6). Решение области вterrainпобитно равноd400_*того же случая, посчитанного с окнами (окна решаются после области и её не меняют) — тест NN-P6. Уточнение 02.10 (без смены версии): если генератор ограничивает итерации несошедшихся решений (предел/ранний отказ — NN-P6), предел записывается вmanifest.jsonиplan.json(solver.max_iterили аналог), образец сохраняется и при статусеmax(как в v1), статус и число итераций — в метаданных случая; тест побитного равенства «область без окон =d400_*с окнами» — при одинаковом пределе. - v2: пилот читает несколько наборов (список в
config.yaml:mainv1 +terrainv2); id случаев уникальны между наборами (префиксы мест разные); окна набора v1 пилот не читает. - Контрактный тест (
tools/research/air_nn_pilot/tests/test_contract_sample.py): ключи, формы, dtype, конечность, совпадениеaglи мест сplan.jsonна образцах набора (или на мини-наборе smoke); v2 — оба варианта (с окнами и только область) поmeta.contract. - v3 (03.10; решение пользователя по Q2 — вариант Б): цель несошедшегося решения — среднее поздних состояний.
Для решения (
hилиmотдельно) со статусомmax(не сошлось доsolver.max_iter) генератор сохраняет вd400_*среднее арифметическое снимков полей решения (u, v, w, θ′ — все каналы) на итерацияхlate_from, late_from + late_step, …, max_iter(конфигconfigs/dataset.yaml, по умолчанию 500…1000 через 50 — 11 снимков; снимки берутся без правки решателя); сошедшееся решение — конечное состояние, как в v2.d400_hc,d400_H,d400_hbl— как v2. В метаданных каждого решения —target(final|late_mean),late_n(число снимков, 1 дляfinal),late_spread60_p90(м/с; собственный разброс решения: для каждой клетки области безedge_cellsна 60 м (линейно между 50 и 75 м) — RMS по снимкам |Δ(u, v)| от среднего, затем 90-й процентиль по клеткам; 0 дляfinal). Вmanifest.json/plan.json—solver.late_mean = {from, step},meta.contract=П1 v3. Повтор случая — побитно тот же файл. Наборmainv1 не пересчитывается: его решенияmax— с целью «последнее состояние» (потребитель считаетtarget = lastдля v1 по статусуmax,final— для сошедшихся). Контрактный тест — + вариант v3 (ключи метаданных,late_n= числу снимков поsolver.late_meanуmax, 1 у сошедшихся). Уточнение 03.10 (без смены версии): решение со статусомdiverged—target = final(как сошедшееся по форме, в оценке — группа несошедшихся);late_fromиlate_stepкратны 10 (решатель зовёт обратный вызов раз в 10 итераций).
П2. Вход и выход сети области пилота (версия 5)
Владелец: NN-P2. Потребители: оценка и отчёт пилота (NN-P2), экспорт ONNX (замер ORT).
Поворот: на k·90° так, чтобы ветер дул «с запада» в секторе ±45°; остаток угла — в числа (cos, sin). Поворот вращением; отражение поперёк ветра — только как аугментация обучения (v4, ниже). Обратный поворот выхода — точный (тест: поворот ×4 = тождество).
Карты входа (float32, фиксированные нормировки, не статистика набора): рельеф
hc − mean(hc)/1000 м, поток теплаd400_H/800 Вт/м²; прочие карты — по выбору NN-P2, только из того, что есть у игры без решателя (рельеф, вода, солнце), с записью в README.v2 (02.10, по итогу NN-P2): + координатные карты x′, y′ клетки в повёрнутой системе (сторона притока области — граничное условие решателя; игре известны тривиально); 18 чисел FiLM, θ′ в К без масштаба — список в README пилота.
Числа (FiLM): U10/10, cos/sin остатка угла, признак и параметры нагрева/устойчивости из метаданных (
profile,day: z_i, z_lcl над средним рельефом /1000 м, α, max_profile, высота солнца), — список и нормировки в README.Выход: 13 высот × 7 каналов = 91 карта 96 × 96 в повёрнутой системе: без нагрева (u∥, u⊥, w), с нагревом (u∥, u⊥, w, θ′). Скорости — отклонение от профиля притока (по
profile), делённое на max(U10, 1 м/с); θ′ — в К (или на конвективный масштаб — с записью). Обратное преобразование в м/с и К в исходной системе — одна функция, ею пользуются оценка и отчёт.Деление (детерминированно, по хешу id и зерну из конфига): (б)
ongudaiцеликом вне обучения и проверки; (а) доля случаев каждого места обучения — «новые условия»; (в) обучение на 5/10/20/40 рельефах из пула (встроенные кромеongudai+ синтетика + процедурные), вложенными подмножествами, оценка — наongudaiи отложенных процедурных рельефах.v3 (02.10, этап П-2; решение пользователя): карты входа — 9 (float32, 96 × 96, повёрнутая система, порядок
prep.MAP_NAMES), нормировки — фиксированные константы (не статистика набора и не min-max случая). ê′ = (cos r, sin r) — точное направление «куда дует» в повёрнутой системе (r — остаток поворота), ê′⊥ = (−sin r, cos r) — влево от потока; ∇h — центральные разностиnp.gradient(hc′, 400 м)(у края — односторонние); G_σ — гауссово сглаживаниеscipy.ndimage.gaussian_filter(hc′, σ/400 м, mode="nearest").# имя формула нормировка 0 terrainhc′ − mean(hc′) /1000 м 1 heat_fluxd400_H/800 Вт/м² 2, 3 x,yцентр клетки x′, y′ /19 200 м 4 slope_along∇h · ê′ (> 0 — склон поднимается по ветру: наветренный) /0,3 5 slope_cross∇h · ê′⊥ /0,3 6 tpi_2khc′ − G_σ(hc′), σ = 2000 м (> 0 — гребень/вершина) /300 м 7 tpi_8khc′ − G_σ(hc′), σ = 8000 м /1000 м 8 shelterSx (Winstral 2002): max по d ∈ {200, 400, …, 4000 м} от atan((h(p − d·ê′) − h(p))/d), h — билинейно по hc′, вне области — ближайшая клетка; > 0 — затенено наветренным рельефом, < 0 — открыто /0,3 рад Константы карт 4–8 (σ, шаг и дальность Sx, нормировки) владелец NN-P5 может изменить один раз до массового счёта с обоснованием через координатора (версия контракта не меняется, если меняются только числа — запись здесь). Числа FiLM (18) и выход (91 канал) — без изменений. Деление v3 (детерминированно по хешу id и зерну): (б)
ongudai— вне обучения и проверки; (г) отложенные горные системы — все места П6 сpart = holdout(главная оценка); (б′) 5 отложенных процедурныхp_*; пул обучения — места П6part = pool+ встроенные кромеongudai+ синтетика + прочие процедурные; (а) у каждого места пула доля случаев «новые условия» (тест) и проверка, остальное — обучение; (в) кривая — вложенные подмножества мест П6 пула размером 25/50/100/200/300 в порядке «по слоямstratumП6 по кругу, внутри слоя — по хешу»; прочие места пула входят в каждую точку; точка с полным пулом = основная сеть (отдельно не обучается); оценка кривой — на (г) и (б).v4 (02.10, решение пользователя): отражение поперёк ветра (y′ → −y′) — точная аугментация обучения. Основание: в решателе (
air3d/air.py) силы Кориолиса нет (f_cor — только модуль для высоты пограничного слоя); если офлайн-решатель получит Кориолис/поворот Экмана — отражение убрать (версия +1). Отражение R применяется к повёрнутому образцу: массивы 96 × 96 разворачиваются по оси j (a[..., ::-1, :]), затем знаки:что правило карты terrain,heat_flux,x,slope_along,tpi_2k,tpi_8k,shelterтолько разворот по j карта yразворот по j и смена знака (итог совпадает с исходной картой y′ — проверяется тестом) карта slope_crossразворот по j и смена знака числа FiLM sin r → −sin r; поперечная составляющая направления на солнце (если есть в числах) → минус; остальные без изменений выход: u⊥ (без нагрева и с нагревом) разворот по j и смена знака выход: u∥, w (оба набора), θ′ только разворот по j Инварианты (тест NN-P5): R∘R = тождество (вход, числа, цель); карты из отражённого рельефа и −r ( prep) = R(картыисходного) — согласованность входа и цели. В обучении — R с вероятностью 1/2 при каждом показе примера, детерминированно от зерна, эпохи и индекса (продолжение после прерывания даёт тот же итог); оценка, проверка, кривая и экспорт — без отражения. Остальное (карты, нормировки, числа, 91 канал) — как v3. v5 (03.10, пилот П-3): физическая кодировка — вход 27 карт (первые 9 = v4), выход 117 каналов (разгон и поворот к линейной базе Б1, отрыв от склона, θ′), упаковка по высоте внутри сети; выбор
encodingв конфиге, v4 остаётся. Полностью —docs/contracts/air-nn-p3.md.
П3. Отчёт пилота (версия 4)
Владелец: NN-P2. Потребители: пользователь (шлюз ШП), координатор.
Один файл
$AIR_NN_DATA/pilot/reports/<дата>_<имя>/report.md(+figures/,metrics.json), строится скриптом.Ключевые числа на 60 м над стартами (интерполяция между 50 и 75 м, билинейно по горизонтали): ошибка ветра (|Δ скорости| с нагревом), ошибка подъёма (w без нагрева и с нагревом), RMS ветра в 1 км от старта; доля случаев с ошибкой ветра < 0,3 м/с и подъёма < 0,1 м/с. Для (а) и (б): сеть против базовых линий — профиль притока без поправки, среднее по набору, регрессия air-lite (метрики
out/fit_metrics.json: rmse/skill тех же целейbuild.pyна фолдах «место ongudai» и «новые условия»; оговорка — air-lite на окнах 100 м, сеть на области 400 м). Кривая (в). 5–8 картинок срезов «решатель / сеть / разница». Время ORT на CPU. Вывод по ориентирам ШП (§8.2 плана) — по числам отчёта, без ручных вставок.v2 (02.10, этап П-2; критерий пользователя): точки оценки — (1) все клетки области без
edge_cellsу края на 60 м над рельефом (линейно между 50 и 75 м); (2) гребни — клетки сtpi_2kслучая ≥ его 90-го процентиля; (3) прежние точкиplan.json → centers. Ошибка ветра — |Δ(u, v)| с нагревом, «ок» при ≤ max(0,3 м/с; 0,1·|V_решателя|); подъём — |Δw| без и с нагревом, «ок» при < 0,1 м/с. Наборы: (г) отложенные горные системы (главное), (б) Онгудай, (б′) отложенные процедурные, (а) знакомые рельефы + новые условия (раздельно — места П6 и прежние), обучающие. Смещение: знаковая ошибка скорости e = |V_сети| − |V_решателя| (с нагревом), среднее по клеткам области — по наборам × 13 высот, × корзины U10 (0–2, 2–4, 4–6, ≥ 6 м/с), отдельно на гребнях на 60 м; то же для w. Правило ШП-2 (числа порогов — вconfig.yaml): на (г) доли «ок» ветра и обоих подъёмов ≥ 90 % и |среднее e| ≤ max(0,1 м/с; 2 % средней |V_решателя|) на высотах ≤ 300 м, в каждой корзине U10 с ≥ 20 случаями и на гребнях (порог смещения max(0,1 м/с; 2 %) подтверждён пользователем 02.10). Кривая (в) 25…300 на (г) и (б): доля «ок» ветра, медианы ошибок, среднее e. Таблица признаков рельефа наборов (уклон 400 м p50/p95, размах) — из П6. Базовые линии, срезы, ORT — как v1.v3 (03.10; решения пользователя по Q2 и Q3): все числа v2 по наборам и кривой — раздельно для сошедшихся и несошедшихся решений (+ число случаев в каждой группе): ошибка ветра и w с нагревом — по статусу решения
h, w без нагрева — по статусуm; цель несошедшихся —targetиз П1 v3 (late_mean, у v1 —last). Для несошедшихся — ещё отношение ошибки ветра сети к собственному разбросу решения (late_spread60_p90): медиана по случаям. Вердикт ШП-2: (1) «отказ от направления», если медиана ошибки ветра сети на (г) (все случаи) ≥refuse_ratio(1,0,config.yaml) × наименьшей из медиан базовых линий на (г) — подтверждено пользователем 03.10; иначе (2) «идём в волну 0», если правило v2 выполнено на сошедшихся решениях (г); иначе (3) «правим подход». Правило v2 на несошедшихся — отдельная строка таблицы ШП-2 (выполнено/нет, доли «ок»), в вердикт не входит: цель несошедшихся шумит сильнее порога (собственный разброс p90 ≈ 0,9 м/с против 0,3 — NN-P6), решение координатора 03.10.v4 (03.10, пилот П-3): + таблица вариантов П-3 и сводка заменимости (ρ = ошибка сети / погрешность решателя), вердикт ШП-3 —
docs/contracts/air-nn-p3.md.
П6. Вырезка места (версия 3)
Владелец: NN-P4 (рельефы). Потребители: NN-P6 (генератор, через places.py), NN-P5 (деление, оценка), NN-P7.
- Каталоги (
$AIR_NN_DATA/pilot/): сырьё —raw/terrarium/<z>/<x>/<y>.png(раскладка кеша игры) +raw/manifest.json(источник, шаблон URL, атрибуция); вырезки —tiles/v3/(v2 —tiles/v2/, v1 —tiles/v1/; не используются после v3):manifest.json(команда, коммит, конфиг и зёрна выбора, источник и лицензия, число мест по частям/слоям/системам, размеры, дата,complete),index.csv,cut/<id>.npz,figures/(гистограммы признаков). Временное —tmp/. - Путь рельефа = рантайм игры (
scripts/terrain/terrarium_loader.gd+configs/world.json → runtime_terrain, слойdetail): уровень z =layer_zoom(zoom 12, понижение при шаге <min_spacing_m18 м), высота R·256 + G + B/256 − 32768 м, узлы — центры пикселей web-mercator, метрическая сетка с шагом пикселя на широте центра (_plan_layer), без сглаживания. Затем — билинейно на сетку 25 м 1601 × 1601 с центром в (lat, lon) (x — восток, y — север, узел (800, 800) — центр) →h; клетки 400 м — блочное среднее 16 × 16 узлов (air3d/terrain.block_mean, область x, y ∈ [−19 200, 19 200] м). Расхождение с игрой (записано, исправляется в NN-4/N1):AirPlace.block_meanберёт f = round(dx/s) узлов слоя рантайма (s ≈ 18–38 м) — клетка f·s ≠ 400 м; пилот считает точные 400 м. Вход сети в игре (N1) обязан давать точные клетки 400 м той же функцией. index.csv(строка на место, порядок — по id):id(t_0000…),lat,lon(°, центр),system(ключ горной системы из конфига илиother),part(pool|holdout),stratum(метка слоя стратификации),zoom,src_spacing_m,h_mean,h_min,h_max(поhc400, м),relief_m(= h_max − h_min),slope_p50,slope_p95(|∇hc400| центральными разностями, м/м, без 1 клетки у края),tpi2k_p95(м),sea_frac(доля узловh≤ 0,5 м),sha256(файла вырезки).cut/<id>.npz:hfloat32 (1601, 1601), м над морем, оси [j — север, i — восток];hc400float64 (96, 96) — блочное среднееh;meta— JSON-строка (lat, lon, zoom, src_spacing_m, список тайлов, версия форматаП6 v1). Значения конечные.- Инварианты: квадраты пула не перекрываются (|Δx| ≥ 38,4 км или |Δy| ≥ 38,4 км — по одной из осей; уточнено 02.10
по вопросу NN-P4) и не ближе 100 км к Онгудаю; места отложенных систем не ближе 50 км к местам пула; доля моря
sea_frac≤ порога конфига; размахrelief_m≤ 3000 м (решение пользователя 02.10, конфиг); отложенные горные системы — Кавказ, Пиренеи, Аппалачи (решение пользователя 02.10) и Южные Альпы Новой Зеландии (v2, решение пользователя 03.10), по 15 мест; один конфиг → побитно те жеindex.csvиcut/*.npz; файл появляется целиком (временное имя → fsync → переименование); стадии продолжаются той же командой. - Загрузчик:
places.location("t_0000")→ объект сh,info(spacing 25, x0 = y0 = −20 000),water = None,meta(center_lat, center_lon),sites(пусто),height_at(x, z)(какSynthLocation);places.context("t_…")— датаreference_context(как у игры для выбранной точки), lat/lon места,utc_offset_h = round(lon/15)иW.ground_context(height_at). Вода — нет (границы пилота: озёра в горах — как суша). - Контрактный тест
tests/test_contract_terrain.py(координатор; владелец дополняет): столбцы и типы индекса, ключи/формы/dtype/конечность вырезок,hc400=block_mean(h)≤ 1e-6 м, sha256, инварианты частей и расстояний. - v2 (03.10; решение пользователя по Q1): + отложенная система Южные Альпы Новой Зеландии (15 мест; на v1 отложенные
системы оказались положе пула — медиана уклона 0,068); перевыбор (пул 300 — с новым правилом «не ближе 50 км к
местам отложенных систем», в т. ч. новозеландских) и нарезка в
tiles/v2/(сырьёraw/общее, докачиваются недостающие тайлы). Форматindex.csv,cut/<id>.npz, загрузчика — без изменений;metaвырезки —П6 v2. Потребители берут путь набора вырезок из конфига (configs/dataset.yaml,configs/terrain.yaml), не зашиваютv1; тесты на настоящем индексе — число отложенных систем и мест из конфига (4 × 15), не константа 45. - v3 (03.10; решение пользователя по Q4 — вариант Б): + инвариант дно квадрата
h_min≤ 3000 м над морем (конфиг; решатель «как игра» и фон погоды откалиброваны до ~3 км — риск §9 плана); перевыбор и нарезка вtiles/v3/(сырьёraw/общее), состав систем и числа мест — как v2 (пул 300, 4 × 15);metaвырезки —П6 v3. Потребители — путь из конфига; контрактный тест — +h_min≤ порога конфига.
П4. Таблица параметров поверхности (версия 1)
Владелец: NN-2а. Потребители: NN-2 (офлайн-решатель, баланс радиации), N1 (каналы α, β, z0 входа сети).
docs/research/surface_params.md(люди) +docs/research/surface_params.csv(конвейер), одна строка на класс ESA WorldCover v200 (код класса: 10 деревья, 20 кустарник, 30 луг, 40 пашня, 50 застройка, 60 голый грунт/скалы, 70 снег/лёд, 80 вода, 90 болото, 95 мангры, 100 мох/лишайник) + строки сезонного снега (snow_fresh,snow_old).- Столбцы CSV:
class_code, class_name, albedo, albedo_min, albedo_max, bowen, bowen_min, bowen_max, z0_m, z0_min_m, z0_max_m, g_frac, g_frac_min, g_frac_max, sources(числа — безразмерные/метры,sources— ключи из списка литературы документа через;). Выбранное значение — внутри диапазона; диапазон = значение, если источник один. - Не код и не вход решателя напрямую: NN-2 решает, как из таблицы строится поток H (§2.3 плана).
П5. Пробный стык ONNX Runtime ↔ Godot (пробник, не N5; версия 2)
Владелец: NN-7а. Потребители: NN-T5, NN-7 (решения по сборке; API N5 будет описан отдельно).
- Каталог
native/air_nn_probe/(исходники,build.sh, README); бинарники ORT и сборки — вне git, путь в README. - Класс
AirNnProbe(имя сProbe— не финальный):load(path: String) -> int(0 — успех, иначе код ошибки),run(input: PackedFloat32Array, shape: PackedInt64Array) -> PackedFloat32Array(вход [1, C, H, W] float32, порядок C-подобный; выход — плоский массив первого выхода модели),last_error() -> String, форма выхода —output_shape() -> PackedInt64Array. Пустой массив — ошибка. - v2 (02.10, по итогу NN-7а): +
set_intra_op_threads(n)/get_intra_op_threads()(по умолчанию 1, действует на следующийload); кодыload: 1 — файл не читается, 2 — ORT не создал сессию, 3 — нет входа/выхода, 4 — не загрузилась библиотека ORT. ORT грузится расширением из своего каталога (Linux —dlmopen(LM_ID_NEWLM)из‑за конфликта символов libstdc++ сaddons/debug_draw_3d). Потребителей пока нет. - После NN-7б (решение пользователя 02.10:
addons/debug_draw_3dудаляется) ожидается обычныйdlopenна Linux — проверяется в NN-7б;dlmopenв пробнике остаётся до её приёмки. - Модель-пустышка — малый Conv (ONNX opset 17), эталон — onnxruntime в Python (venv пилота); совпадение ≤ 1e-5.