Размер поля воздуха и кеш вариантов запуска
Вопрос пользователя: сколько весит рассчитанное поле воздуха (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 + zstd | 5,8–6,4 МБ; ошибка ≤ 0,002 м/с при 3 м/с, до 0,008 м/с при 12 м/с (измерено) |
| Квантование 0,01 м/с (int16, разность по высоте) + zstd | 1,2 МБ (zstd-19) / 1,7 МБ (zstd-3); ошибка ≤ 0,005 м/с |
| Квантование 0,02 м/с + zstd | 0,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 с — таймаут и аналитика |
Главные выводы:
- Полный кеш всех вариантов нереален: тысячи решений на старт, часы расчёта, мегабайты не главное. К тому же любое изменение модели воздуха (а она меняется постоянно) обесценивает весь кеш.
- Огрубить можно только часть параметров: дату (не влияет вообще), небо (две группы), температуру (4 узла), верхние скорости ветра (9–11 м/с по линейной смеси 8 и 12). Направление ветра и час огрублять нельзя: поле у старта по направлению рвётся на масштабе 15°.
- Главная находка не про размер, а про время: при ветре 1–2 м/с, ветре 3 м/с с юга (180°) или запада (270°), штиле в 9:00 решатель упирается в предел 3000 итераций. Расчёт идёт 26–54 с (на 4070 SUPER), в 9:00 в штиль — таймаут 60 с и игра остаётся без поля. Именно такие условия, скорее всего, дают у пилота 39 с загрузки. Кеш именно их ускорит больше всего, но они же дороже всего в предрасчёте.
- Рекомендация: не предрасчёт «всего», а ленивый кеш на диске: после каждой загрузки класть готовое поле (ключ — место, старт, условия, версия модели), при повторе брать из кеша. Размер — 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·50 | 400, 105 | 460 800 | 38,4 × 38,4 км, центр места | AirPlace.domain_case, общая для всех стартов места |
| окно 100 м | 64·64·62 | 100, 50 | 253 952 | 6,4 км вокруг старта | AirWindowCase, центр на старте |
| окно 50 м | 64·64·112 | 50, 25 | 458 752 | 3,2 км вокруг старта | то же |
Область других встроенных мест (измерено по AirPlace.domain_case): Алтай 96·96·42 (387 072 клеток), Аскарово и Аушкуль 96·96·36 (331 776). Высота окон зависит от рельефа окна (для них замера нет — оценка по Онгудаю).
Что лежит в WindField (и что нужно сохранить)
| Величина | Тип | Размер (Онгудай) | Сохранять? |
|---|---|---|---|
| u, v (восток, север) и w_mech (вертикаль без нагрева) — три канала подряд на клетку | float32 | 3 × nx·ny·nz | да |
| w_conv (конвективная вертикаль = w − w_mech) | float32 | nx·ny·nz | да |
| θ′ (отклонение температуры), К | float32 | nx·ny·nz | да |
| hc: рельеф сетки по столбцам | float32 | nx·ny | да |
heat: поток тепла H по столбцам, Вт/м² (meta.heat) | float32 | nx·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,18 | 5,08 | 9,22 | 23,5 |
| float32 + gzip-9 | 6,65 | 3,71 | 7,00 | 17,4 |
| float32 + zstd-19 | 6,63 | 3,71 | 7,00 | 17,3 |
| float32 + xz-9 | 5,17 | 3,01 | 6,27 | 14,5 |
| float16 сырое | 4,59 | 2,54 | 4,61 | 11,7 |
| float16 + gzip-9 | 2,02 | 1,33 | 2,98 | 6,3 |
| float16 + zstd-3 (умолчание Godot) | 2,08 | 1,35 | 2,97 | 6,4 |
| float16 + zstd-19 | 1,81 | 1,22 | 2,81 | 5,8 |
| int16, шаг 0,05 м/с, разность по z, zstd-19 | 0,17 | 0,14 | 0,33 | 0,63 |
| int16, шаг 0,02 м/с, разность по z, zstd-19 | 0,25 | 0,20 | 0,49 | 0,94 |
| то же, zstd-3 | 0,40 | 0,29 | 0,67 | 1,36 |
| int16, шаг 0,02 м/с, без разности, zstd-19 | 0,26 | 0,21 | 0,59 | 1,05 |
| int16, шаг 0,01 м/с, разность по z, zstd-19 | 0,33 | 0,25 | 0,63 | 1,21 |
| то же, zstd-3 / gzip-9 | 0,50 / 0,38 | 0,36 / 0,30 | 0,83 / 0,74 | 1,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_max | 41: 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,86 | 0,06 | 0,61 | 24 % / 15 % |
| 3 → 4 | 1,68 | 0,12 | 1,16 | 39 % / 25 % |
| 5 → 6 | 2,96 | 0,21 | 1,95 | 41 % / 30 % |
| 8 → 10 | 4,97 | 0,41 | 3,13 | 33 % / 29 % |
| направление 150° → 157,5° (7,5°) | 0,95 | 0,07 | 0,74 | 18 % / 19 % |
| 150° → 165° | 1,51 | 0,08 | 1,20 | 33 % / 34 % |
| 150° → 180° | 2,48 | 0,08 | 1,97 | 59 % / 61 % |
t_max 24 → 26 °C | 0,08 | 0,008 | 0,06 | 2 % / 4 % |
| 26 → 30 | 0,08 | 0,009 | 0,05 | 3 % / 7 % |
| 26 → 40 | 0,23 | 0,005 | 0,16 | 7 % / 9 % |
| 20 → 26 | 0,41 | 0,05 | 0,29 | 10 % / 30 % |
| 15 → 20 | 1,27 | 0,26 | 1,32 | 27 % / 107 % |
| 0 → 5 → 10 | 0 (побитно одно поле) | 0 | 0 | 0 |
| небо ясно → переменное | 0,15 | 0,015 | 0,12 | 4 % / 5 % |
| ясно → облачно (12:00) | 0,89 | 0,05 | 0,52 | 23 % / 14 % |
| час 12 → 15 | 0,06 | 0,02 | 0,07 | 3 % / 9 % |
| час 9 → 12 | 0,37 | 0,15 | 0,75 | 29 % / 112 % |
| час 15 → 20 | 0,77 | 0,01 | 0,43 | 23 % / 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…12 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 12; 9–11 смесью 8 и 12 | 9 м/с: 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…40 | 15, 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,3 | 4 (в 12 и 15 ч хватает 3: 15, 20, 26) |
| облачность | 3 | ясно (+ переменная), облачно | переменная вместо ясно: 0,15 / 0,015; облачно в 20:00 вместо ясно: 0,05; облачно при ≥ 8 м/с вместо ясно: ≤ 0,16 | 2 (в 20 ч — 1) |
| час старта | 4 | 4 (оставить) | 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,6 | 8,3 | 10,3 | [230, 200] |
| 12:00, 8 м/с с 150° | 18,0 | 7,8 | 10,2 | [120, 130] |
| 12:00, 6 м/с с 270° | 17,5 | 7,3 | 10,2 | [110, 110] |
| 20:00, 3 м/с с 150° | 17,5 | 6,9 | 10,6 | [140, 140] |
| 15:00, 4 м/с с 90° | 20,1 | 6,7 | 13,4 | [100, 90] |
| 12:00, 3 м/с с 180° | 50,3 | 40,0 | 10,3 | [3000, 2150] |
| 12:00, 2 м/с с 270° | 63,3 | 53,3 | 10,0 | [3000, 3000] |
| 12:00, 1 м/с с 270° | 64,4 | 54,1 | 10,3 | [3000, 3000] |
| 9:00, штиль | 68,4 | 60,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 = 0 | 7,5 (10 итераций: решение без нагрева тривиально) |
| 12:00, 150°, U = 1 и 2 | 30 и 44 (3000 итераций — предел) |
| 12:00, 270°, U = 0 / 1 / 2 / 3 / 4–12 | 7,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 = 3 | 10,3–11,7 (340–440 итераций); T ≥ 28 — 5,4–6,7 |
| 12:00, U = 0, T = 10 | 29 (3000 итераций у решения с нагревом) |
| 12:00, U = 3, 270°, T = 10 | 60 — таймаут, поля нет |
| 9:00, штиль | 60 — таймаут, поля нет |
| 15:00, U = 0 | 9,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 = 3 | A + 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. Три пути: кеш вариантов, суррогат, расчёт как сейчас
| Расчёт как сейчас | Кеш вариантов (предрасчёт «всего») | Ленивый кеш (по факту запусков) | Суррогат (формулы / мода) | |
|---|---|---|---|---|
| Объём на диске | 0 | 2,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. Подводные камни кеша
- Версия модели. Ключ обязан включать хеш
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) → предрасчитанный «весь кеш» после каждого выпуска начинается с нуля, а на слабой машине это часы. - Рельеф. Область зависит от слоя
detail(хеш данных). Для точек карты рельеф качается из сети (runtime_terrain), кеш DEM у пилота; значит, ключ — ещё и версия тайлов; предрасчёт возможен только после первой загрузки точки (в «сохранённом месте» — сразу, но рельеф уже на диске). - Окна привязаны к старту, область — к месту. Один старт — одно место; для 10 встроенных стартов — 10 наборов окон, 4 набора областей. Область можно хранить отдельно и делить (50 % объёма: область 0,49–0,63 МБ из 1,2 МБ).
- Окна и тёплый старт. После загрузки из кеша нет
parent_dataи_warm: сдвиг окон за пилотом не работает до первого пересчёта (через 15 игровых минут или сразу, если запустить фоновый пересчёт), а сам пересчёт идёт холодным и на слабом ветре — те же 25–50 с GPU в фоне. Либо хранить MAC-состояние области (4,5 МБ float16 + zstd на вариант), либо принять. - Расчёт может не сойтись или не успеть. Решения с 3000 итераций — «поле как есть» (не эталон); таймаут 60 с — поля нет. В кеш такие варианты класть нельзя без решения, что считать результатом; предрасчёт всей сетки на таких условиях длится по минуте на вариант и часть вариантов падает.
- Детерминизм поля. Игровой кеш фиксирует поле, посчитанное на конкретной видеокарте. В сети каждый клиент считает поле сам (ключ мира,
Game.start); с кешем клиенты окажутся на полях, посчитанных на разных GPU или в разное время (разные версии кеша). Сейчас это уже так, но с кешем различия сохранятся между запусками. - Прочее входное.
u10и направление — из атмосферы (прогноз ± «встречный на старте»),t_maxи небо — из прогноза пилота; в ключ их нужно класть точно (округление, например u10 до 0,01, направление до 0,1°),needs_recomputeиспользует допуски 0,05 м/с и 1°. - Подача. После чтения из кеша нужно выставить
_curи_cur_key(иначе первый же_processрешит, что поле не соответствует, и запустит пересчёт),meta.cond, и пересобрать источники термиков (AirThermals.build, ~0,8 с на главном потоке по документации: их нет в кеше). - Диск и обслуживание. Нужен лимит (LRU), очистка при смене версии, атомарная запись, защита от обрыва. Для аудитории «семья и друзья» (память
Tiny audience) — достаточно простого LRU по 50–100 записям.
8. Рекомендация
- Не делать предрасчёт всей сетки вариантов при сохранении места. 2 160 вариантов на старт, 2–3 ГБ, 4,5–6,5 часов на 4070 SUPER и в разы дольше у пилота; кеш пропадает при каждой правке модели воздуха, а правки идут каждую неделю. Огрубление помогает лишь отчасти (57 564 → 2 160): направление и час нельзя огрублять, слабый ветер дорог.
- Сделать ленивый кеш на диске. После успешной загрузки класть три уровня в
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. - Дополнительно (по желанию пользователя): предрасчёт «типичных» условий при сохранении места, в фоне и с возобновлением. 12:00, ясно, 26 °C, скорости 2–4 м/с, 9 направлений = 27 вариантов ≈ 10 минут на 4070 SUPER (из них 7 минут — 9 вариантов при 2 м/с по 45–50 с; оценка по моим замерам; у пилота 20–40 минут) и ≈ 32 МБ (квантование) – 160 МБ (float16). Это уже «чаще всего нужное»; остальное — лениво. Вариант без предрасчёта (только лениво) проще и достаточен.
- Отдельная, более значимая задача (не кеш): решатель не сходится за 3000 итераций на слабом ветре. Условия, которые дают 25–60 с загрузки или таймаут: ветер 1–2 м/с, 3 м/с с юга/запада, 9:00 в штиль. Выигрыш от починки — для всех запусков, не только повторных, и это, скорее всего, и есть 39 с у пилота. Что стоит проверить: критерий остановки (
tol_*, поле «как есть» после предела), число итераций предела, поведение тёплого старта между соседними скоростями. Не мой скоуп, передаю как находку. - Если пользователь всё-таки хочет предрасчёт: сначала получить у пилота строку
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).