AM-00: база для сравнения масштабов 1–3 (модель воздуха)

Задача AM-00 (docs/plan/air_model.md → «Задачи → M0»), снято 29.09.2026 в worktree ~/deltaplan-wf, ветка feature/air-model, коммит на момент снятия — см. «Коммиты» в отчёте координатору. Числа ниже — точка отсчёта для сравнения масштабов 2 и 3 в AM-07/AM-08 (±10–20 % из плана) и для смежных задач (AM-01…AM-06). Инструмент, которым сняты числа §2–3 (переиспользуется): tools/research/air_model_baseline/probe.gd + .tscn.

1. Headless-прогон (tools/check.sh / res://tests/run_tests.tscn)

Команда:

godot --headless --path . --import
godot --headless --path . res://tests/run_tests.tscn

(временный профиль: XDG_DATA_HOME=$(mktemp -d) godot …)

Итог: 609 тестов, 4 упало, 1 пропущено (пропуск — новый test_gpu_smoke.gd::test_compute_smoke, см. §4).

Упавшие:

ТестСоответствует известной нерегрессии из задания?
test_atmosphere.gd::test_performanceда — порог времени под нагрузкой
test_net_screen.gd::test_client_backend_create_embedded_vs_remoteда — порт 8080
test_menu_setup.gd::test_loading_screen_follows_progressнет, новая — см. ниже
test_net_screen.gd::test_nearby_listнет, новая — см. ниже

Из известного списка не упали в этом прогоне: test_compute_time_40km, test_forest_mask_loaded (пороговые тесты под нагрузкой — не воспроизвелись именно сейчас, ожидаемо нестабильны от прогона к прогону, а не «стали лучше»).

Две новые FAIL — не регрессия от AM-00. Worktree общий (несколько агентов пишут в один индекс параллельно, git status на момент снятия показывал незакоммиченные правки другого агента: scripts/ui/flight_setup_screen.gd, scripts/game/launch_options.gd, scripts/world/sun_clock.gd, configs/world.json, tests/ui/test_menu_setup.gd, tests/game/test_catch_up_game.gd — по журналу, это AM-06 часть А, часы старта). test_loading_screen_follows_progress падает с тем же SCRIPT ERROR: Nonexistent function 'start_hours'/'nearest_start_hour', что валит и test_cockpit.gd в этом прогоне (тот не входит в 4 FAIL только потому что падение там — внутри _fly(), тест ловит через check, а не крашится) — прямое следствие незавершённой правки flight_setup_screen.gd у соседнего агента, не код AM-00. test_net_screen.gd::test_nearby_list не в списке изменённых файлов соседнего агента; похоже на сетевую нестабильность в песочнице того же рода, что и известный порт 8080 (LAN-обнаружение), а не на новую регрессию кода — не чинил, не в скоупе AM-00. Обе — координатору К0: пересобрать headless-набор после того, как AM-06 часть А закоммитит start_hours/nearest_start_hour.

2. test_ridge_starts (уже в tests/atmosphere/test_ridge_starts.gd, зелёный)

Таблица набора уже задокументирована и не менялась (docs/guide/atmosphere.md → «Склон на стартах»):

СтартНабор за 5 мин, м
altai/sinyukha_west134
altai/sinyukha_east242
altai/tugaya_south−85 (слабый/узкий склон)
askarovo/biyagoda_west195
askarovo/biyagoda_east65
askarovo/biyagoda_south_west261
aushkul/aushtau_east126
aushkul/aushtau_south56
aushkul/ridge_west−25 (слабый/узкий склон)
ongudai/kayancha_south225

3. air_velocity_at — время вызова

Онгудай, старт kayancha_south, 75 м AGL над рабочей точкой, ветер 20 км/ч в склон, 100 000 вызовов подряд (после разогрева поля одним step):

14,3–14,9 мкс/вызов (три прогона: 14,05 / 14,93 / 14,26; варьирует из-за шума песочницы, не кода — берём медиану ≈ 14,3 мкс).

4. FPS в полёте

Снят существующим tools/bench/frame_bench.sh (не новый инструмент, есть в репозитории), локация ongudai, пресет графики medium, три отметки времени полёта × 2 ракурса:

Ракурс@отметкасредний FPS1 %-low FPS
кабина@2с117,043,7
сзади@2с112,865,7
кабина@30с112,351,1
сзади@30с112,167,3
кабина@60с89,315,3
сзади@60с112,814,7

Машина — с реальным GPU (RTX 4070 SUPER), не типичная у пилотов; число — только точка отсчёта для сравнения «до/после» на этой же машине, не абсолютный ориентир.

5. Подветренная зона: СКО вертикали и частота «рывков» на 3 стартах

Инструмент: tools/research/air_model_baseline/probe.gd. Метод: Atmosphere (та же схема, что test_ridge_starts._make_atmo/_scan_lee_point) над рельефом старта; термики выключены (weather static, пусто — «поле» ещё не существует, эффект тот же, что требуемое «поле выключено»); ветер 20 км/ч, перевёрнутый (дует с обратной стороны хребта) — та же схема, что в test_lee_zone_all_starts; точка — самая глубокая под линией тени в радиусе 1 км от старта (поиск _scan_lee_point). В найденной точке неподвижно (пилот не летит, зонд стоит) 180 с симуляционного времени на 20 Гц (3600 отсчётов) снимается air_velocity_at(pos).y; сид атмосферы фиксирован (seed = 42), место и час — из configs/locations/*.json (без AtmoDay, погода weather/medium постоянна — не «час», а «типичный день»).

Определение «рывка» — выброс модуля мгновенной вертикальной производной dw/dt (конечная разность по отсчётам 1/20 с) за 3·σ(dw/dt) ряда самой производной. Порог самоопределяемый (не абсолютное число): не зависит от общего уровня болтанки, что и нужно для сравнения «до/после» в AM-08 (план требует ±10–20 % от базы, не абсолютные пороги). Частота — событий в минуту.

Три старта с явной подветренной зоной (выбраны так, чтобы не попасть в WEAK_LEE_SITES из test_ridge_starts.gd, где линия тени неглубокая и эффект слабый):

Стартветер, км/ччас/сидСКО w, м/срывков/минпорог |dw/dt|, м/с²глубина тени, м
altai/sinyukha_west20 (с обратной стороны)сид 42, погода medium1,13918,01,954165,7
askarovo/biyagoda_west20 (с обратной стороны)сид 42, погода medium1,0757,31,92458,6
aushkul/aushtau_east20 (с обратной стороны)сид 42, погода medium1,27426,02,02479,0

Воспроизвести:

XDG_DATA_HOME=$(mktemp -d) godot --headless --path . \
  res://tools/research/air_model_baseline/probe.tscn -- \
  --out=tools/research/air_model_baseline/out/baseline.json

JSON с теми же числами — tools/research/air_model_baseline/out/baseline.json (не в git, считается заново каждым прогоном; воспроизводимость — по фиксированному сиду в скрипте).

6. Термики за день на 2 местах (ongudai, aushkul)

Тот же скрипт. Метод: постоянная погода weather/medium (без хода дня/AtmoDay — «типичный день», упрощение по правилу «не усложняем» для базы; ход дня можно добавить позже, если AM-07/AM-08 это потребуют), термики dynamic, фокус — первый старт места, радиус генерации по умолчанию (20 км). 4 «игровых» часа симуляционного времени шагом 5 с (2880 шагов): накапливаются все рождённые термики (сила и потолок — на момент снятия из AtmoThermal, не меняются после рождения кроме огибающей).

Расстояние до соседа — не по всем термикам за 4 часа (один источник переживает ~15–20 циклов за это время — «соседями» были бы предыдущие/следующие термики той же точки, а не реально соседние работающие термики), а по срезам: раз в 60 с симуляционного времени берутся термики, живые одновременно (atmo.field.thermals на этот момент), и для каждого — расстояние до ближайшего другого живого в этот момент; усреднено по всем срезам.

ЛокацияN термиков за 4 чсила, м/с (ср.)потолок AGL, м (ср.)расстояние до соседа, м (ср., N срезов)
ongudai13 4882,411794,7586,4 (227 381)
aushkul13 2262,421788,3602,7 (222 952)

Сверка со здравым смыслом: сила у нижней границы пресета weather/medium (2,4–4,4 м/с) — ожидаемо, среднее по всем термикам смещено вниз показателем sun_strength_exponent/освещённостью источников, а не только пиком; потолок ≈ cloudbase_agl_m пресета (1800 м) — термики почти всегда достают до кромки, что для «хорошего дня» ожидаемо; расстояние до соседа ≈ 0,7–0,75 × thermal_spacing_m (800 м) — источники на сетке клеток, но не все клетки одновременно «горят», так что реальный интервал между работающими термиками немного меньше шага сетки — физически разумно.

7. GPU-тесты и headless-пропуск (приёмка AM-00)

  • tests/test_case.gd: TestCase.needs_gpu() -> bool (по умолчанию false), переопределяется в тестах, которым нужен настоящий RenderingDevice.
  • tests/run_tests.gd: флаг --gpu; тест с needs_gpu() == true без флага — пропущен (счётчик «пропущено», не «упало»); с флагом — выполняется как обычно.
  • tests/atmosphere/test_gpu_smoke.gd (заглушка): RenderingServer.create_local_rendering_device(), тривиальный compute-шейдер (v[i] = v[i]*2+1 на буфере из 64 float), сверка результата.
  • tools/gpu_tests.sh: godot --path . --audio-driver Dummy --resolution 320x240 res://tests/run_tests.tscn -- --gpu, временный XDG_DATA_HOME.

Проверено:

  • headless (--headless, без --gpu): RenderingServer.create_local_rendering_device() → null → тест пропущен, 0 тестов, 0 упало, 1 пропущено при фильтре test_gpu_smoke; полный прогон — 609 тестов, 4 упало, 1 пропущено (см. §1).
  • tools/gpu_tests.sh --filter=test_gpu_smoke (под flock /tmp/heat_ca_gpu.lock, реальный дисплей, Vulkan/RTX 4070 SUPER): 1 тестов, 0 упало, 0 пропущено, тест проходит.

Файлы

  • tools/research/air_model_baseline/probe.gd, probe.tscn — сбор §3, §5, §6 (новые).
  • tools/gpu_tests.sh — новый.
  • tests/atmosphere/test_gpu_smoke.gd — новый.
  • tests/test_case.gd, tests/run_tests.gd — добавлен needs_gpu()/--gpu (правки).
  • docs/guide/atmosphere.md → «Склон на стартах» — таблица §2 не менялась, переиспользована.