Размер поля воздуха и кеш вариантов запуска

Вопрос пользователя: сколько весит рассчитанное поле воздуха (GPU-модель scripts/atmosphere/air_model/) и можно ли при сохранении места заранее посчитать поля для всех вариантов запуска и положить их в кеш, чтобы загрузка была быстрой. Дополнения: какие параметры меню можно огрубить, чтобы вариантов было меньше; можно ли заменить расчёт формулой (суррогатной моделью).

Состояние кода: main a554502 (1 октября 2026), без правок ветки feature/air-start. Все замеры — Онгудай, старт kayancha_south (151°), GPU RTX 4070 SUPER, CPU Xeon E5-2666 v3 (одно ядро медленнее, чем у Ryzen 5 4500 пилота), если не сказано иное. Что измерено, а что оценено — помечено в каждом разделе. МБ = 10⁶ байт.

Коротко

ЧтоЧисло
Готовое поле одного варианта (3 уровня: область 400 м + окна 100 и 50 м)сырое float32 23,5 МБ (+0,14 МБ рельеф и поток тепла)
Сжатое без потерь (float32 + zstd/gzip)17,3 МБ (почти не сжимается: шум младших разрядов)
float16 + zstd5,8–6,4 МБ; ошибка ≤ 0,002 м/с при 3 м/с, до 0,008 м/с при 12 м/с (измерено)
Квантование 0,01 м/с (int16, разность по высоте) + zstd1,2 МБ (zstd-19) / 1,7 МБ (zstd-3); ошибка ≤ 0,005 м/с
Квантование 0,02 м/с + zstd0,94 МБ / 1,36 МБ; ошибка у старта ≤ 0,013 м/с
Чтение из кеша вместо расчётараспаковка 10–30 мс + сборка WindField 0,8 с (рабочий поток) + источники термиков (по оценке из docs/guide/air-model.md, ~0,8 с на главном потоке); итого ~1–2 с против 7–8 с типичных
Вариантов в меню на один старт57 564 (4 часа × 13 скоростей × 9 направлений × 41 температура × 3 неба)
После огрубления по данным ниже≈ 2 160 на старт (направление и час огрублять нельзя)
Объём при этом (квантование 0,02–0,01)2,3–2,9 ГБ на старт, 17–29 ГБ на 10 встроенных стартов (нижняя граница — область общая у стартов одного места); float16 — в 5 раз больше (13 ГБ на старт)
Время предрасчёта на 4070 SUPER~4,5–6,5 ч на старт (2 160 × медиана 7,7 с … среднее 10,5 с по моей выборке); у пилота — по оценке в 1,5–3 раза дольше, с провалами до 60 с
Расчёт при загрузке сейчас7–8 с почти везде (этап «Рассчитываем ветер» 7–9 с из 18,6 с всей загрузки), но 25–60 с при слабом ветре и некоторых направлениях; при 60 с — таймаут и аналитика

Главные выводы:

  1. Полный кеш всех вариантов нереален: тысячи решений на старт, часы расчёта, мегабайты не главное. К тому же любое изменение модели воздуха (а она меняется постоянно) обесценивает весь кеш.
  2. Огрубить можно только часть параметров: дату (не влияет вообще), небо (две группы), температуру (4 узла), верхние скорости ветра (9–11 м/с по линейной смеси 8 и 12). Направление ветра и час огрублять нельзя: поле у старта по направлению рвётся на масштабе 15°.
  3. Главная находка не про размер, а про время: при ветре 1–2 м/с, ветре 3 м/с с юга (180°) или запада (270°), штиле в 9:00 решатель упирается в предел 3000 итераций. Расчёт идёт 26–54 с (на 4070 SUPER), в 9:00 в штиль — таймаут 60 с и игра остаётся без поля. Именно такие условия, скорее всего, дают у пилота 39 с загрузки. Кеш именно их ускорит больше всего, но они же дороже всего в предрасчёте.
  4. Рекомендация: не предрасчёт «всего», а ленивый кеш на диске: после каждой загрузки класть готовое поле (ключ — место, старт, условия, версия модели), при повторе брать из кеша. Размер — 1,2–6 МБ на вариант, сотня вариантов — до 600 МБ в float16. Плюс (отдельная задача решателя) посмотреть на не сходящиеся прогоны: это выигрыш больше, чем любой кеш. Подробности — в конце.

1. Что такое «готовое поле»

После AirRuntime.load_field() в атмосфере лежит набор из трёх WindField (AirFieldSet.levels, от мелкого к грубому). Всё, что решатель считает на GPU, превращается в эти массивы (AirPicardJob.field_async → WindField.from_mac), остальное строится из них на CPU.

Сетки (измерено, Онгудай)

УровеньСетка nx·ny·nzΔx, Δz, мКлетокПокрытиеОткуда
область96·96·50400, 105460 80038,4 × 38,4 км, центр местаAirPlace.domain_case, общая для всех стартов места
окно 100 м64·64·62100, 50253 9526,4 км вокруг стартаAirWindowCase, центр на старте
окно 50 м64·64·11250, 25458 7523,2 км вокруг стартато же

Область других встроенных мест (измерено по AirPlace.domain_case): Алтай 96·96·42 (387 072 клеток), Аскарово и Аушкуль 96·96·36 (331 776). Высота окон зависит от рельефа окна (для них замера нет — оценка по Онгудаю).

Что лежит в WindField (и что нужно сохранить)

ВеличинаТипРазмер (Онгудай)Сохранять?
u, v (восток, север) и w_mech (вертикаль без нагрева) — три канала подряд на клеткуfloat323 × nx·ny·nzда
w_conv (конвективная вертикаль = w − w_mech)float32nx·ny·nzда
θ′ (отклонение температуры), Кfloat32nx·ny·nzда
hc: рельеф сетки по столбцамfloat32nx·nyда
heat: поток тепла H по столбцам, Вт/м² (meta.heat)float32nx·nyда — без него источники термиков не строятся (AirThermals.has_inputs)
gam: dθ̄/dz по уровням, z_i, u10, wdir, z0, геометрия сетки, меткачислабайтыда (meta, формат AirCase.meta())
первая воздушная клетка столбца, 1/ln(a₁/z₀), u*, «внешний» ветер за гребнем, w*, толщина слоя — по столбцам——нет: from_arrays пересчитывает за 0,2–0,3 с на уровень
коэффициент турбулентной вязкости K, давление p——в WindField их нет. K для масштаба 3 считается из поля на лету (turb_at)
источники термиков——нет: строятся из поля при подаче (ThermalField, AirThermals.build, по документации ~0,8 с на области)

То есть «готовое поле» = пять float32-каналов на трёх сетках (2,3 млн значений на уровень области) + малые массивы по столбцам. Пять каналов, а не «u, v, w, θ′, K»: K в поле не хранится.

Что считается в полёте и от времени (в кеш не идёт)

  • Пересчёт поля каждые air_model.recompute_game_min = 15 игровых минут (час слота) и при смене ветра (> 0,05 м/с или 1°) или погоды. Старт всегда в слоте (9, 12, 15, 20 ч — границы слотов), так что кеш стартового поля годится только до первого пересчёта.
  • Сдвиг окон за пилотом (ушёл на ¼ стороны окна), фоном: нужно состояние решения области (AirPicardJob.parent_data(): сетка, типы клеток, u, v, w, θ′, θ′_d для решений с нагревом и без, в сетке с ореолом, 22 МБ сырых: 11 массивов по 2 МБ; zstd float32 — 12,1 МБ, float16 + zstd — 4,5 МБ). Из кеша WindField его не восстановить: либо хранить отдельно, либо сразу после загрузки из кеша запустить обычный фоновый пересчёт (при нём окна переставятся с центром у пилота).
  • Источники термиков, пузыри, болтанка, подвод air_velocity_at — строятся из полей и времени, кешу не принадлежат.
  • Тёплый старт (_warm) для первого пересчёта: без него первый пересчёт — холодный, как загрузка; для пилота невидим (фон), но на слабом ветре это те же 20–50 с GPU в фоне.

Есть ли сохранение и загрузка поля из файла

  • Загрузка есть: WindField.load_file(путь) — <путь>.json (метаданные, arrays: {имя: [смещение, длина]} в числах float32, version = 1) + <путь>.bin (float32 LE). Массивы: u, v, w_mech, w_conv, theta, hc, по желанию heat. Ключ запуска --air-field=<путь>: читает Atmosphere один раз за запуск, один уровень (не набор), и тогда AirRuntime вообще не считает (CMD_FIELD).
  • Сохранения в игре нет. Формат пишет только питоновский конвертер tools/research/air3d/to_game_field.py (поле прикидки на 5 МБ). Для кеша нужны: писатель (GDScript), чтение набора из трёх уровней, ключ условий и подача в AirRuntime вместо расчёта.
  • Формат сырой float32 без сжатия: кеш им хранить нельзя (23,5 МБ на вариант), нужен собственный упаковщик (zstd есть в Godot: PackedByteArray.compress(FileAccess.COMPRESSION_ZSTD)).

2. Размер одного варианта: сырой и сжатый (измерено)

Реальное поле: Онгудай, 12:00, 3 м/с с 150°, T = 26 °C, ясно; три уровня, дамп через AirRuntime (скрипт ниже). Планарная раскладка каналов (u, v, w_mech, w_conv, θ′ подряд), сжатие — zstd CLI (уровни 3 и 19), gzip -9, xz -9.

ПредставлениеУровень 50 мУровень 100 мОбласть 400 мТри уровня
float32 сырое9,185,089,2223,5
float32 + gzip-96,653,717,0017,4
float32 + zstd-196,633,717,0017,3
float32 + xz-95,173,016,2714,5
float16 сырое4,592,544,6111,7
float16 + gzip-92,021,332,986,3
float16 + zstd-3 (умолчание Godot)2,081,352,976,4
float16 + zstd-191,811,222,815,8
int16, шаг 0,05 м/с, разность по z, zstd-190,170,140,330,63
int16, шаг 0,02 м/с, разность по z, zstd-190,250,200,490,94
то же, zstd-30,400,290,671,36
int16, шаг 0,02 м/с, без разности, zstd-190,260,210,591,05
int16, шаг 0,01 м/с, разность по z, zstd-190,330,250,631,21
то же, zstd-3 / gzip-90,50 / 0,380,36 / 0,300,83 / 0,741,7 / 1,4
hc + heat (float32, zstd-19)+0,11

Другие условия дают тот же порядок: шаг 0,02 + zstd-19 для 12:00 с 8 м/с — 1,17 МБ, штиль — 0,82, 9:00 — 0,95, 20:00 — 0,95 (compress2.json; float16 + zstd-19 везде 5,5–6,2 МБ).

Что это значит:

  • float32 не сжимается ни одним методом (24 → 17 МБ): младшие разряды — шум решателя.
  • float16 + zstd — 6 МБ и почти ничего не теряет: максимум ошибки 0,002 м/с при 3 м/с (полшага float16 растёт с величиной: 0,004 для значений 8–16 м/с и до 0,008 для 16–32 м/с), пилотные метрики (ветер и подъём у старта на 30/60/150 м, подъём в радиусе 1 км) меняются на ≤ 0,001 м/с.
  • Квантование — 1 МБ. Шаг 0,02 м/с даёт ошибку у старта ≤ 0,013 м/с (подъём на 60 м) и ≤ 0,009 м/с (ветер), относительную ошибку подъёма по окну 2 %; шаг 0,01 — ≤ 0,005 м/с, 1 %; шаг 0,05 — до 0,03 м/с, 5 %. Для вариометра (разрешение 0,1 м/с) годятся все; разумно 0,01–0,02.
  • Разность по высоте даёт ещё ~15–20 % к квантованию.
  • Распаковка (измерено, Godot): zstd 3–11 мс на уровень; float16 → float32 через Image.convert (FORMAT_RH → FORMAT_RF) 6–14 мс на уровень. Квантование в GDScript (decode_s16, накопление разности) — 0,46 с на уровень области (2,3 млн значений, Xeon; на Ryzen, по оценке, вдвое меньше): ~1,2 с на три уровня в рабочем потоке. float16 быстрее на порядки, но в 5 раз больше на диске.
  • Сборка WindField.from_arrays из массивов — 0,31 + 0,17 + 0,30 с на три уровня (рабочий поток, Xeon).
  • Состояние области для сдвигов окон (parent_data): 22 МБ сырых, 12 МБ zstd float32, 4,5 МБ float16 + zstd (измерено).

3. Что такое вариант запуска, какие параметры влияют на поле

Что входит в условия расчёта

AirRuntime.conditions_of берёт: час неба, ветер атмосферы на 10 м (м/с), направление «откуда», дневной максимум температуры пилота (t_max), небо. Плюс место и старт. Остальное из меню на поле не влияет.

ПараметрЗначения в менюВлияет на поле?Комментарий
место (встроенное или точка на карте)4 встроенных, любые точки картыдаобласть зависит от места и рельефа. Для точек карты рельеф качается из сети, центр — выбранная точка: предрасчёт без пилота невозможен, кеш только после первого запуска
площадка старта1–3 на место, всего 10только окнаобласть общая у всех стартов места; окна 100/50 м центрируются на старте
час старта4: 9, 12, 15, 20 (world.json → time.start_hours)да, сильнодискретный список; все 4 нужны
скорость ветра13: 0…12 м/с шагом 1 (weather_model.json → ui.wind_ms)да, сильнов меню целые, но через CLI/ключ мира можно любое
направление ветра8 румбов + «встречный» (= курс площадки), compass_points = 8да, сильнодо 9 значений на старт (из них 8 общие на место); через --from= / ключ мира — любые градусы
температура дня t_max41: 0…40 °C шагом 1да, ступенькойменяет толщину слоя перемешивания z_i и поток тепла
облачность3: ясно, переменная, облачнода, слабо и не везде
дата (месяц, число)любыенетполе считается на опорную дату (reference_context = 15 июля, Р14 в docs/archive/plan/air-model-progress.md); AirPlace.context дату полёта не берёт
крыло, масса, боты, сид мира—нетв conditions_of не входят

Полная сетка меню: 4 × 13 × 9 × 41 × 3 = 57 564 варианта на старт (в расчёт — 3 уровня каждый).

Чувствительность: насколько меняется поле при шаге параметра (измерено)

Метод. Базовая точка: Онгудай, старт Каянча, 12:00, 3 м/с с 150°, 26 °C, ясно. Для каждой пары полей: разность скорости ветра по модулю и вертикальной скорости (w_mech + w_conv) в колонке старта на 60 м над землёй (ветер у старта там 4,53 м/с, подъём +0,38 м/с); среднеквадратичная разность в радиусе 1 км от старта на той же высоте; относительная L2-разность по всему окну 100 м до 600 м над землёй (как в tools/research/air3d/reference.md, п. 8). Уровень 50 м для точек у старта, 100 м для «по окну». Порог заметности для пилота взят из docs/research/air-model-sensitivity.md: 0,3 м/с для ветра, 0,1 м/с для подъёма (вариометр).

Прогоны — 155 решений через AirRuntime (холодный старт, как при загрузке); поля побитово воспроизводятся (три повтора базовой точки совпали до нуля).

Шаг параметраΔ ветра у старта, м/сΔ подъёма у старта, м/сRMS ветра в 1 кмотносительная разность по окну: ветер / подъём
скорость 2 → 3 м/с0,860,060,6124 % / 15 %
3 → 41,680,121,1639 % / 25 %
5 → 62,960,211,9541 % / 30 %
8 → 104,970,413,1333 % / 29 %
направление 150° → 157,5° (7,5°)0,950,070,7418 % / 19 %
150° → 165°1,510,081,2033 % / 34 %
150° → 180°2,480,081,9759 % / 61 %
t_max 24 → 26 °C0,080,0080,062 % / 4 %
26 → 300,080,0090,053 % / 7 %
26 → 400,230,0050,167 % / 9 %
20 → 260,410,050,2910 % / 30 %
15 → 201,270,261,3227 % / 107 %
0 → 5 → 100 (побитно одно поле)000
небо ясно → переменное0,150,0150,124 % / 5 %
ясно → облачно (12:00)0,890,050,5223 % / 14 %
час 12 → 150,060,020,073 % / 9 %
час 9 → 120,370,150,7529 % / 112 %
час 15 → 200,770,010,4323 % / 21 %

Что видно:

  • Скорость и направление двигают всё, зависимость нелинейная. Ветер на 60 м растёт быстрее U: отношение к U10 = 1,5 при 3–5 м/с, 1,77 при 6 м/с, 2,0 при 12 м/с; скачок между 5 и 6 м/с (7,64 → 10,60 м/с). Подъём при штиле 0,12, при 1 м/с 0,27, при 3 м/с 0,38.
  • Температура ниже 15 °C не влияет совсем: слой перемешивания упирается в минимум (zi_min = 300 м), поля для 0, 5, 10 °C побитово одинаковы; 15 → 20 °C — резкая ступень (в w по окну 107 %: включается конвекция); выше 24 °C поле почти не меняется на 12:00 (≤ 0,23 м/с на 14 градусов).
  • Температура зависит от часа: в 9:00 и 20:00 она действует и выше 26 °C (26 → 35 °C — 0,79 м/с в 9:00 и 0,39 м/с в 20:00), в 12:00 нет.
  • Облачность значима только днём на слабом ветре: в 12:00, 9:00, 15:00 при 3 м/с ≈ 0,8–0,9 м/с, в 20:00 — 0,05; при 8 м/с — 0,1–0,16 м/с. «Переменная» почти не отличается от «ясно».
  • Час 12 и 15 при 3 м/с различаются на 0,06 м/с, но при 6–8 м/с — на 0,26–0,47 м/с; свести их нельзя бесплатно.
  • Эти числа — по одной точке (Каянча, 151° — склон ЮЮВ), по одному параметру за раз; в 9:00 и 20:00 проверены только скорость 3 м/с и температура, в 12:00 — всё. Для других стартов — оценка «порядок тот же».

Огрубление: какие параметры можно сузить и что это стоит (измерено на тех же прогонах)

Ошибка — разность между точным полем и полем, собранным линейной смесью двух ближайших узлов (по всему набору полей по формуле (1 − w)·A + w·B), на высоте 60 м над стартом.

ПараметрЧто в менюПредложенная сеткаОшибка огрубления (среднее / наибольшее: ветер, м/с; подъём, м/с)Вариантов
даталюбаявыкинуть0 (не входит в расчёт)×1
крыло, масса, сид—выкинуть0×1
направление ветра8 румбов + встречныйоставить как есть (9)Смесь между узлами 15°: 0,18 / 0,87 м/с, 22,5°: 0,30 / 1,17, 30°: 0,33 / 0,81, 45°: 0,58 / 1,42, 90°: 0,91 / 1,79 при 3 м/с; при 8 м/с узлы через 30°: 1,27 / 3,25. Подъём в той же смеси ошибается в среднем на ≤ 0,03 м/с, наибольшая ошибка до 0,13 м/с9
скорость ветра0…120, 1, 2, 3, 4, 5, 6, 7, 8, 12; 9–11 смесью 8 и 129 м/с: 0,03 / 0,00; 10: 0,013 / 0,001; 11: 0,02 / 0,003. Выкинуть 5 нельзя (скачок 5 → 6: 0,76 / 0,05), 7 — на грани (0,29–0,46 / 0,02). Масштабирование поля по U вместо смеси хуже (3 → 4: 0,17; 3 → 6: 1,5; 8 → 10: 1,35 м/с)10
температура0…4015, 20, 26, 35 (ниже 15 — поле 15; выше 35 — поле 35; между — смесью)на 12:00 (узлы 15, 20, 26, 33): 24 °C — 0,08 / 0,007; 28 °C — 0,02; 30 °C — 0,02; 36 °C — 0,07; 40 °C — 0,14; 10 °C — 0,15 / 0,011. В 9:00 и 20:00 между узлами не проверялось — оценка ≤ 0,34 (в 12 и 15 ч хватает 3: 15, 20, 26)
облачность3ясно (+ переменная), облачнопеременная вместо ясно: 0,15 / 0,015; облачно в 20:00 вместо ясно: 0,05; облачно при ≥ 8 м/с вместо ясно: ≤ 0,162 (в 20 ч — 1)
час старта44 (оставить)12 → 15 бесплатно только при слабом ветре, при 8 м/с 0,47 м/с4

Остаётся: сочетаний «час × небо × температура» 8 + 6 + 6 + 4 = 24; × 10 скоростей × 9 направлений = 2 160 вариантов на старт (из 57 564). Если ещё склеить 12 и 15 ч (ошибка до 0,5 м/с при 8 м/с) — 1 620.

Дополнительная экономия, которую данные позволяют, но которую я не закладывал: облачность и температура при скорости ≥ 8 м/с почти не влияют (разность ≤ 0,2 м/с на 8 м/с против 0,9 при 3 м/с), т. е. на сильном ветре их тоже можно склеить.

Можно ли что-то применять поверх готового поля без пересчёта

  • Ограничители (max_speed_ms, max_w_ms) — да, уже накладываются при подаче (WindField.clamp_values).
  • Ветер: масштабирование по скорости — нет (ошибки выше). Смесь двух полей по скорости — да при U ≥ 8 (≤ 0,03 м/с), грубо ниже. По направлению — нет (полосы 15°).
  • Температура и небо: смесь соседних узлов — да, ошибки выше; это единственные непрерывные ручки, у которых смесь работает.
  • Дата, крыло, масса: вне поля вообще.

4. Время расчёта сейчас

На нашей машине (измерено)

Этап «Рассчитываем ветер» (AirRuntime.load_field: вход места и подготовка в рабочем потоке 1,2 с → решатель области с нагревом и без нагрева на GPU → сборка поля → окна 100 м и 50 м). Базовая точка (12:00, 3 м/с, 150°): 6,9–7,2 с стены (три прогона под flock), GPU 1,7–1,9 с (230 + 190 итераций области, окна по 40–50 итераций), окна 0,45 + 0,67 с стены. Расчёт полностью детерминирован.

Разбивка загрузки места целиком (tools/loading/load_probe.gd, Онгудай, окно 1280 × 720; без flock, GPU делили с чужими фоновыми прогонами, но базовая точка совпала с замером под flock — 8,3 с против 7,0):

УсловияВсего, сЭтап ветра, сОстальное, сИтерации области
12:00, 3 м/с, встречный (151°) — по умолчанию18,68,310,3[230, 200]
12:00, 8 м/с с 150°18,07,810,2[120, 130]
12:00, 6 м/с с 270°17,57,310,2[110, 110]
20:00, 3 м/с с 150°17,56,910,6[140, 140]
15:00, 4 м/с с 90°20,16,713,4[100, 90]
12:00, 3 м/с с 180°50,340,010,3[3000, 2150]
12:00, 2 м/с с 270°63,353,310,0[3000, 3000]
12:00, 1 м/с с 270°64,454,110,3[3000, 3000]
9:00, штиль68,460,0, таймаут8,4аналитика (air_model: analytic (таймаут расчёта (60 с)))

Остальные этапы загрузки (деревья 2,7 с, объекты 2,8 с, самолёт 3,3 с, меш 0,4 с, погода 0,35 с и т. д.) не зависят от ветра и дают ≈ 10 с.

Зависимость времени этапа ветра от условий (измерено; 155 решений, GPU 4070 SUPER, часть прогонов шла параллельно с чужими)

УсловияВремя расчёта, с (итерации области, окон не считал)
12:00, 150°, U = 3…12 м/с7,0–9,9 (120–230 итераций)
12:00, 150°, U = 07,5 (10 итераций: решение без нагрева тривиально)
12:00, 150°, U = 1 и 230 и 44 (3000 итераций — предел)
12:00, 270°, U = 0 / 1 / 2 / 3 / 4–127,5 / 49,5 / 48,8 / 25,8 / 6,1–9,0
12:00, U = 3, 8 румбов (0, 45, …, 315°)7,2–12,3, кроме 180° (48,8) и 270° (25,8)
12:00, U = 3, 48 направлений через 7,5°медиана 8,2; 5 направлений дольше 15 с: 172,5° (34), 180° (49), 187,5° (31), 262,5° (23), 270° (26)
12:00, U = 6, 8 румбов6,1–7,5, кроме 180° (26,2)
12:00, U = 8, 24 направления через 15°6,3–8,8, медленных нет
T = 26 → 0…20 °C при U = 310,3–11,7 (340–440 итераций); T ≥ 28 — 5,4–6,7
12:00, U = 0, T = 1029 (3000 итераций у решения с нагревом)
12:00, U = 3, 270°, T = 1060 — таймаут, поля нет
9:00, штиль60 — таймаут, поля нет
15:00, U = 09,0
20:00, любая скорость6,1–8,9

Итог по выборке из 155 решений (она смещена к слабому ветру и неудобным направлениям, поэтому «в среднем по меню» цифры будут лучше): медиана 7,7 с, среднее 10,5 с, 13 решений (8 %) дольше 20 с, из них 2 — таймаут (60 с, поля нет). Время и 3000 итераций — у решения области (с нагревом или без): оно не сошлось за предел, но поле всё равно отдаётся (если успело до 60 с).

У пилота (оценка, замера нет)

RX 5600 XT: пропускная способность памяти 288–336 ГБ/с против 504 ГБ/с у 4070 SUPER; решатель упирается в память, в плане модели оценка для AMD среднего класса — ×2 (docs/plan/air_model.md, «AMD»). Ryzen 5 4500 по одному потоку быстрее нашего Xeon примерно в 1,3–1,5 раза.

Часть этапа ветраЗдесь, сУ пилота, с (оценка)
вход места и подготовка (CPU) + сборка поля + окна (CPU-часть)≈ 3,5–4≈ 2,5–3
решатель на GPU (быстрые условия)1,3–2,5≈ 3–6
итого, быстрые условия7–8≈ 6–10
итого, условия до 3000 итераций26–54 (GPU 16–35)≈ 40–110 → упирается в таймаут 60 с

Загрузка 39 с у пилота при 18,6 с у нас по умолчанию: либо у него всё примерно вдвое медленнее (этап ветра ≈ 15 с из 39), либо условия из «медленной» группы (этап ветра 25–35 с из 39). Выбрать между этими вариантами по имеющимся данным нельзя. Нужна строка из лога пилота вида air_model: поле 12:00 ч, 3.0 м/с с 151° (загрузка): X с, итераций [..] (или analytic (таймаут …)) и его условия (ветер, направление, час).

5. Можно ли заменить расчёт формулой (суррогатом) — прикидка

По просьбе пользователя — без POD/SVD на полях и без новых больших прогонов: только данные, что уже есть (155 решений выше, docs/research/air-model-sensitivity.md, tools/research/air3d/reference.md п. 8). Это грубая оценка, а не обоснование.

Прямая аппроксимация величин у старта (60 м над землёй, 12:00):

ВеличинаФормулаОшибкаВывод
подъём w от скорости, U ≥ 3, 150°квадратичная по U (3 коэф.)≤ 0,05 м/с на диапазоне 0,38…2,0; степенной закон w ∝ U^1,24 — ≤ 0,06работает
то же, 270°квадратичная≤ 0,02 м/с; w ∝ U^1,45работает
подъём w от направления, U = 3A + B·cos(θ − θ₀) (3 коэф.)СКО 0,04, наибольшая 0,13 м/с при размахе −0,29…+0,46работает: вертикаль у склона почти косинус направления (склон ЮЮВ)
подъём w от направления12 гармоник (25 коэф.)СКО 0,012, наибольшая 0,036да, но уже не «формула»
ветер от скорости, U ≥ 3степенной закон U^1,2 / кубический полином0,5–0,9 м/с при значениях 4,5…24 м/сне работает из-за скачка 5 → 6 м/с; ниже 3 м/с — совсем нет (U = 0, 1, 2, 3: 0,75, 2,7, 3,7, 4,5 м/с)
ветер от направлениягармоники по θСКО 0,30 м/с (K = 1…3), 0,16 (K = 6), 0,10 (K = 12) при размахе 3,7…5,1не работает: за соседним холмом полосы 15–30° (4,78 → 3,80 → 4,52 м/с при 15°, 30°, 45°)
любая величина от температурылюбой полиномступень 15 → 20 °C: 1,27 м/с / 0,26 м/с; ниже 15 — константанет: нужна кусочная таблица

Все поле целиком. Смесь двух готовых полей (то, что в разделе 3 называлось огрублением) — это уже поле-суррогат: по направлению с шагом 15° средняя ошибка ветра у старта 4 % (0,18 из 4,5 м/с), наибольшая 19 %; по скорости между узлами 8 и 12 — 0,01–0,03 м/с. Из прежних исследований: ближайшее поле библиотеки по направлению — 50–75 % ошибки, смесь двух ближайших через 45° — 8–15 % (reference.md, п. 8); погода 18 вместо 26 °C — 28–44 %. Суррогат с главными модами (POD + интерполяция коэффициентов) — не проверялся: оценка. Чтобы построить модель, нужно столько же решений (сотни, ради такой же сетки параметров), а размер модели — число мод × 1 МБ: даже 20 мод — 20 МБ на старт; выигрыша в размере против сжатого кеша из 2 160 вариантов (2–3 ГБ) много, но он куплен ошибкой и теми же часами обучающих расчётов.

Где формула ломается физически (оценка по физике модели и данным AM-01/AM-09, не по моим замерам, кроме отмеченного):

  • Срыв за гребнем и ротор: возвратный поток и размер зоны отрыва зависят от скорости и направления нелинейно (docs/research/air-model-sensitivity.md: у следа за гребнем σ ≈ μ*; λ/h, local_k и замыкание дают S до 7,7). Ветер у склона при смене направления на 15° падает на 20 % (измерено выше: 4,78 → 3,80 м/с).
  • Режим по числу Фруда: при устойчивой стратификации (утро и вечер, N ≈ 0,01 с⁻¹ в тестах AM-01) Fr = U/(N·H) для хребта H ≈ 300–400 м даёт ≈ 0,8–1 при 3 м/с и ≈ 2 при 8 м/с — переход «блокировка → обтекание» между ними; в 9:00 ветер на 60 м над стартом при U = 3, 6, 8, 12 м/с — 4,4; 9,9; 14,3; 24,4 м/с (отношение к U: 1,47; 1,65; 1,79; 2,04), то есть режим меняется со скоростью, но где именно проходит граница по Фруду, не измерялось.
  • Смена режима конвекции: при 15 → 20 °C на 12:00 включается слой перемешивания (ступень в w по окну 107 %); при слабом ветре с нагревом решатель вообще не сходится (3000 итераций): решение там — нестационарное, и «формулы» не существует (поле «дышит», СКО w ≈ 0,07 м/с по AM-01 для 9:00 в штиль).
  • Нелинейность по направлению из-за рельефа: окно по направлению рвётся на масштабе 15° (см. выше): разложение по гармоникам требует десятков членов.

Вывод. Формула реальна только для узких величин и в узких режимах: подъём у склона как косинус направления и квадратичная функция скорости при U ≥ 3. Для ветра у старта, для рельефа с тенью холмов, для слабого ветра и для температуры она не работает. Заменять расчёт формулой для всего поля нельзя; опора — на кеш или на сам расчёт.

6. Три пути: кеш вариантов, суррогат, расчёт как сейчас

Расчёт как сейчасКеш вариантов (предрасчёт «всего»)Ленивый кеш (по факту запусков)Суррогат (формулы / мода)
Объём на диске02,3–2,9 ГБ на старт (квантование), ×5 в float16; 17–29 ГБ на 10 встроенных стартов1,2–6 МБ на запущенный вариант20–200 МБ на старт (оценка)
Время загрузки поля7–8 с типично, 25–60 с на слабом ветре1–2 с (при попадании в сетку)1–2 с при повторе; первый запуск — как сейчас~0,5 с (оценка)
Предрасчётнет4,5–6,5 ч на старт на 4070 SUPER (2 160 × 7,7–10,5 с), у пилота оценка 10–20 ч и сотни вариантов упрутся в таймаут 60 снетсотни расчётов для обучения (часы)
Ошибка0 (эталон модели)0 в узлах; между узлами скорости/температуры 0,01–0,3 м/с; по направлению огрублять нельзя0 (только точные условия)0,5–1 м/с (10–25 %) на ветре у старта; подъём 0,05 м/с в узких режимах; вне режимов — неизвестно
Сложность0писатель + читатель + ключ + упаковка + фоновый предрасчёт + очередь + прогресс + инвалидация + место на дискеписатель + читатель + ключ (малый)отдельный исследовательский проект; в игре — ещё код вычисления
Срок жизни—вся сетка устаревает при любой правке модели (ключ — хеш кода и конфигов)устаревает так же, но вырастает заново самаустаревает и требует переобучения
Что не покрывает—точки карты (рельеф заранее неизвестен), нестандартные ветер/направление/температура из ключа мирато же, но не нужно заранеевсё, что вне режима

7. Подводные камни кеша

  1. Версия модели. Ключ обязан включать хеш scripts/atmosphere/air_model/*.gd и *.glsl, configs/atmosphere.json → air_model, configs/weather_model.json, шероховатость и профиль ветра (WindProfile), данные рельефа. Иначе после обновления игра отдаёт старую физику. Проект меняет модель воздуха постоянно (А1, А2, Б1, Б2, feature/air-start прямо сейчас правит AirRuntime/AirCase) → предрасчитанный «весь кеш» после каждого выпуска начинается с нуля, а на слабой машине это часы.
  2. Рельеф. Область зависит от слоя detail (хеш данных). Для точек карты рельеф качается из сети (runtime_terrain), кеш DEM у пилота; значит, ключ — ещё и версия тайлов; предрасчёт возможен только после первой загрузки точки (в «сохранённом месте» — сразу, но рельеф уже на диске).
  3. Окна привязаны к старту, область — к месту. Один старт — одно место; для 10 встроенных стартов — 10 наборов окон, 4 набора областей. Область можно хранить отдельно и делить (50 % объёма: область 0,49–0,63 МБ из 1,2 МБ).
  4. Окна и тёплый старт. После загрузки из кеша нет parent_data и _warm: сдвиг окон за пилотом не работает до первого пересчёта (через 15 игровых минут или сразу, если запустить фоновый пересчёт), а сам пересчёт идёт холодным и на слабом ветре — те же 25–50 с GPU в фоне. Либо хранить MAC-состояние области (4,5 МБ float16 + zstd на вариант), либо принять.
  5. Расчёт может не сойтись или не успеть. Решения с 3000 итераций — «поле как есть» (не эталон); таймаут 60 с — поля нет. В кеш такие варианты класть нельзя без решения, что считать результатом; предрасчёт всей сетки на таких условиях длится по минуте на вариант и часть вариантов падает.
  6. Детерминизм поля. Игровой кеш фиксирует поле, посчитанное на конкретной видеокарте. В сети каждый клиент считает поле сам (ключ мира, Game.start); с кешем клиенты окажутся на полях, посчитанных на разных GPU или в разное время (разные версии кеша). Сейчас это уже так, но с кешем различия сохранятся между запусками.
  7. Прочее входное. u10 и направление — из атмосферы (прогноз ± «встречный на старте»), t_max и небо — из прогноза пилота; в ключ их нужно класть точно (округление, например u10 до 0,01, направление до 0,1°), needs_recompute использует допуски 0,05 м/с и 1°.
  8. Подача. После чтения из кеша нужно выставить _cur и _cur_key (иначе первый же _process решит, что поле не соответствует, и запустит пересчёт), meta.cond, и пересобрать источники термиков (AirThermals.build, ~0,8 с на главном потоке по документации: их нет в кеше).
  9. Диск и обслуживание. Нужен лимит (LRU), очистка при смене версии, атомарная запись, защита от обрыва. Для аудитории «семья и друзья» (память Tiny audience) — достаточно простого LRU по 50–100 записям.

8. Рекомендация

  1. Не делать предрасчёт всей сетки вариантов при сохранении места. 2 160 вариантов на старт, 2–3 ГБ, 4,5–6,5 часов на 4070 SUPER и в разы дольше у пилота; кеш пропадает при каждой правке модели воздуха, а правки идут каждую неделю. Огрубление помогает лишь отчасти (57 564 → 2 160): направление и час нельзя огрублять, слабый ветер дорог.
  2. Сделать ленивый кеш на диске. После успешной загрузки класть три уровня в user://air_cache/<ключ>.bin (float16 + zstd: 5,8–6,4 МБ, декодирование 40 мс; либо квантование 0,01–0,02 м/с: 1–1,4 МБ, декодирование ≈ 1,2 с в рабочем потоке), при повторе места, старта и условий брать из кеша: загрузка этапа ветра 7–60 с → 1–2 с. Лимит — 50–100 записей. Ключ — хеш версии модели + место + рельеф + старт + (час, u10, направление, t_max, небо). Сложность небольшая: писатель/читатель набора уровней + ключ + подмена расчёта в AirRuntime.load_field.
  3. Дополнительно (по желанию пользователя): предрасчёт «типичных» условий при сохранении места, в фоне и с возобновлением. 12:00, ясно, 26 °C, скорости 2–4 м/с, 9 направлений = 27 вариантов ≈ 10 минут на 4070 SUPER (из них 7 минут — 9 вариантов при 2 м/с по 45–50 с; оценка по моим замерам; у пилота 20–40 минут) и ≈ 32 МБ (квантование) – 160 МБ (float16). Это уже «чаще всего нужное»; остальное — лениво. Вариант без предрасчёта (только лениво) проще и достаточен.
  4. Отдельная, более значимая задача (не кеш): решатель не сходится за 3000 итераций на слабом ветре. Условия, которые дают 25–60 с загрузки или таймаут: ветер 1–2 м/с, 3 м/с с юга/запада, 9:00 в штиль. Выигрыш от починки — для всех запусков, не только повторных, и это, скорее всего, и есть 39 с у пилота. Что стоит проверить: критерий остановки (tol_*, поле «как есть» после предела), число итераций предела, поведение тёплого старта между соседними скоростями. Не мой скоуп, передаю как находку.
  5. Если пользователь всё-таки хочет предрасчёт: сначала получить у пилота строку air_model: из лога (какая у него ветка: быстрая или медленная), потом выбирать между «лениво» и «типичные условия».

9. Воспроизведение и данные

Исходные данные и скрипты — в рабочем каталоге сессии (не в репозитории): ~/projects/.tmp/claude-1000/-home-greg-deltaplan/55d419f6-4839-418d-8f65-ded00754411a/scratchpad/ — копия проекта dp/ (без .git, build, tools/research) с добавленным каталогом dp/tools/air_cache/ (дамп probe.gd, замеры decode_bench.gd, qdec_bench.gd, grids.gd), правка dp/tools/loading/load_probe.gd (ключи --wind, --from, --temp, --sky), анализ — an/*.py, дампы полей (≈ 4 ГБ) — out/scan/*.bin, сводки — an/all_runs.json, an/main*.json, out/base/compress_*.json.

Суть дампа (в AirRuntime-пути, в точности как делает игра):

# probe.gd (фрагмент): холодная загрузка, поле набора уровней — в файлы
rt.setup(atmo, {detail = detail, water = water, loc = loc}, cond_fn)
rt.set_focus(null, Vector3(st.x, detail.sample(st.x, st.y), st.y))   # окна на старте
rt._cur = {}; rt._cur_key = ""                                       # холодный старт, как при загрузке
await rt.load_field()
for f: WindField in atmo.air_field.levels:                           # [окно 50, окно 100, область]
    # f.raw_vel() (u,v,w_mech подряд), f.raw_w_conv(), f.raw_theta(), f.raw_hc(), f.heat_flux(), f.gam()

Команды (из dp/, XDG_DATA_HOME=$(mktemp -d), окно 320 × 240, не headless: нужен настоящий RenderingDevice):

# дамп поля и время (набор условий: имя:час,u10,откуда,t_max,небо; ';' между прогонами)
XDG_DATA_HOME=$(mktemp -d) flock /tmp/heat_ca_gpu.lock godot --path . --audio-driver Dummy --resolution 320x240 \
  res://tools/air_cache/probe.tscn -- --out=OUT --loc=ongudai --parent \
  --runs="b0:12,3,150,nan,clear;u8:12,8,150,26,clear" 
# полная загрузка с разбивкой по этапам
XDG_DATA_HOME=$(mktemp -d) flock /tmp/heat_ca_gpu.lock godot --path . --audio-driver Dummy --resolution 1280x720 \
  res://tools/loading/load_probe.tscn -- --location=ongudai --hour=12 --wind=3 --from=180 --timeout=150 --no-shots
# распаковка: zstd, float16 через Image, WindField.from_arrays (без GPU)
godot --headless --path . res://tools/air_cache/decode_bench.tscn -- --dir=OUT --name=b0
# сжатие: zstd CLI (-3, -19), gzip -9, xz -9 над планарными каналами; квантование int16 с разностью по z
python an/compress.py OUT b0

Данные и лицензии: поля — собственные расчёты модели; рельеф data/terrain/* и конфиги — из репозитория (источники и лицензии — ASSETS.md).