Б2 «α и λ в игре: профиль притока по устойчивости, пересчёт эталонов и цена» — записка
Волна Б модели воздуха, исполнитель Б2, ветка air/b2 (feature/air-model 00ae28a + air/b1). Контракт — C2 v4
(docs/contracts/air-model.md). Код, данные, команды — tools/research/b2/README.md.
Коротко
- Одна функция профиля
WindProfile(scripts/atmosphere/wind_profile.gd, static) для решателя (AirPlace.domain_case,AirWindowCase→AirCase.p.alpha,p.max_profile) и аналитического ветра (WindModelчерезAtmosphere). Эталон —tools/research/air3d/wind_prof.py(те же таблицы), его вызываетreal.case→real.make. - α по устойчивости: α = 0,24 · r(класс Паскуилла–Тёрнера), r — Irwin 1979 к D. max_profile = (z_sat/10)^α,
z_sat = 0,3 · 0,3 u*/f (правило
rules.py, C10 v3). λ:lam40 м,lam_frac0,0158. - Конфиг:
wind.shear_exponent_neutral0,24 иwind.z_sat_frac0,3;shear_exponentиmax_profile_factorудалены. - Эталон и игра для Онгудая 12:00 дают одно α/max_profile (
test_air_place): 0 м/с — A, 0,112 / 1,149112; 3 м/с — B, 0,112 / 1,404479; 6 м/с — D, 0,240 / 2,445407 (игра и эталон совпадают до 1e-9). - Днём профиль мягче прежнего, в нейтраль — круче. Полдень, ясно, 3 м/с: на 300 м 4,2 м/с вместо 4,8; облачно или 20:00 — 6,2; 6 м/с в любой час — 13,6 вместо 9,7.
- Устойчивые классы E/F (вечер после ≈ 20:25) с нейтральным z_sat давали на 300 м до 14 × U10 (C2 v4). По решению К2 (C2 v5, § 4) z_sat для E, F — по толщине устойчивого слоя 0,4 √(u*L/f), L по Golder 1972: при 3 м/с на 300 м 2,14 × (E) и 1,97 × (F) U10.
- Цена: штиль с нагревом на 200 м — 1101 итерация эталона вместо 251 (×4,4, как ждали по Б1); прочее — § 5.
1. Функция профиля
WindProfile.alpha(u10, sun_elev_deg, cover) -> float
WindProfile.max_profile(alpha, u10, z0, f_cor) -> float # (z_sat/10)^α
WindProfile.z_sat(u10, z0, f_cor) -> float # z_sat_frac · 0,3 · κ·max(u10, 0,5)/ln(10/z0) / f
WindProfile.stability_class(u10, sun_elev_deg, cover) -> int # 0..5 = A..F
WindProfile.net_radiation_index(sun_elev_deg, cover) -> int # −2..4
WindProfile.sun_elevation(ctx, hour) -> float # SunClock.solar_position, без запаздывания прогрева
WindProfile.apply_to_case(c: AirCase, ctx, hour, cover) # c.p.alpha, c.p.max_profile по ветру случаяКласс устойчивости — таблица Тёрнера (Turner 1964; таблица и правила индекса радиации — EPA-454/R-99-005, табл. 6-4, 6-5; Pasquill 1961). Строки — ветер на 10 м в узлах, столбцы — индекс радиации NRI; 1 = A … 7 = G (G → F):
| узлы \ NRI | 4 | 3 | 2 | 1 | 0 | −1 | −2 |
|---|---|---|---|---|---|---|---|
| 0–1 | A | A | B | C | D | F | G |
| 2–3 | A | B | B | C | D | F | G |
| 4–5 | A | B | C | D | D | E | F |
| 6 | B | B | C | D | D | E | F |
| 7 | B | B | C | D | D | D | E |
| 8–9 | B | C | C | D | D | D | E |
| 10 | C | C | D | D | D | D | E |
| 11 | C | C | D | D | D | D | D |
| ≥ 12 | C | D | D | D | D | D | D |
NRI: сплошная облачность (1,0) — 0; ночь — облачность ≤ 0,4 → −2, иначе −1; день — класс инсоляции по высоте солнца (> 60° — 4, 35–60° — 3, 15–35° — 2, ≤ 15° — 1), облачность > 0,5 — минус 2, не ниже 1.
Решения, которых нет в таблице (обоснование — в комментариях кода):
- Ночь Тёрнера («час до заката — час после восхода») задана высотой солнца < 8°: в июле на 50° с. ш. через час после восхода солнце ≈ 9°, за час до заката ≈ 7° (Онгудай: 20:00 — 10,8°, день; 20:30 — 6,4°, ночь).
- Облачность игры —
WeatherModel.sky_params(sky).cover: ясно 0, переменная 0,3, облачно 0,85. Нижней границы облаков в игре нет; «> 0,5 — минус 2» — ветка Тёрнера для границы ниже 7000 футов (кучевые и слоистые). Перистая пелена (cirrus_cover) в класс не входит — у Тёрнера высокая облачность почти не меняет класс. - Нижний предел U10 = 0,5 м/с только для z_sat (порог трогания анемометра, EPA-454/R-99-005); в штиль ветра нет, профиль решателю не нужен.
- r по Irwin 1979, сельская местность: A 0,07, B 0,07, C 0,10, D 0,15, E 0,35, F 0,55 → r = 0,47; 0,47; 0,67; 1; 2,33; 3,67; α = 0,112; 0,112; 0,16; 0,24; 0,56; 0,88.
Где ставится:
- решатель:
AirPlace.domain_caseиAirWindowCase.window_at—WindProfile.apply_to_case(час случая, облачность дняAirPlace.day(...).cover, ветер случая; z0 и f — изc.p).AirCase.p.max_profileпо умолчанию NAN — правило z_sat при α случая (AirCase.max_profile_used), какParams.max_profile = Noneв air.py (синтетика фикстурref/); - эталон:
air.Params(alpha=0,24, max_profile=None, lam_frac=0,0158);real.caseпишетc.alpha, c.max_profile(wind_prof.for_hour),real.makeставит их вParams(ref_study.py, генераторы фикстур,lee_fields.py— все черезreal); - аналитический ветер:
WindModel.set_conditions(sun_elev_deg, cover)иset_windпересчитывают α и предел (z0 0,1 м и f —AirCase.Z0,F_COR);Atmosphereберёт высоту солнца часа из погоды (WeatherModel.derive → _derived.sun_elev_deg, новое поле; пресет без часа —clouds.sun_elevation_deg) и облачность прогноза (_derived.sky), обновляет приconfigure, шаге хода дня (_apply_day) и мягком обновлении погоды.roughness_height_m(пол профиля 1 м) и множитель высоты над стартом (altitude_*) — без изменений. - λ:
AirCase.LAM_FRAC0,0158 (одно значение у решателя и масштаба 3FieldTurbulence, как требует AM-09б),p.lam40.
2. α и max_profile для Онгудая (15 июля, UTC+7)
Из tools/research/b2/out/profile_table.md (эталон) = out/game_profile.txt (игра) — совпадают во всех строках.
Ветер, переменная облачность — как ясно (0,3 ≤ 0,5).
| час (солнце) | ясно: 0 / 3 / 6 м/с | облачно: 0 / 3 / 6 м/с |
|---|---|---|
| 9 (32,1°) | B 0,112 / C 0,160 / D 0,240 | C 0,160 / D 0,240 / D 0,240 |
| 12 (56,8°) | A 0,112 / B 0,112 / D 0,240 | C 0,160 / D 0,240 / D 0,240 |
| 15 (55,1°) | A 0,112 / B 0,112 / D 0,240 | C 0,160 / D 0,240 / D 0,240 |
| 20 (10,8°) | C 0,160 / D 0,240 / D 0,240 | C 0,160 / D 0,240 / D 0,240 |
| 20:30 (6,4°) | F 0,880 / F 0,880 / D 0,240 | F 0,880 / E 0,560 / D 0,240 |
max_profile (z_sat: штиль — 35 м по пределу 0,5 м/с, 3 м/с — 208 м, 6 м/с — 415 м): α 0,112 — 1,149 / 1,404 / 1,518; α 0,16 — 1,220 / 1,625 / —; α 0,24 — — / 2,071 / 2,445. Устойчивые (C2 v5, z_sat по устойчивому слою): E 0,56 при 3 м/с — z_sat 38,9 м, 2,138; F 0,88 — штиль z_sat 8,8 м, 0,896; 3 м/с — 21,6 м, 1,971; 5 м/с — 27,9 м, 2,468.
Сверка с rules.py (C10 v3) на контрольных входах — до 1e-9 (числа записаны в test_c2_params_match_reference):
(α 0,24; 3 м/с; z0 0,1; f 1,13e-4) → z_sat 207,539 м, max_profile 2,070637; (0,235; 8,8; 0,03; 1,23e-4) → 443,372 м, 2,437757.
3. Профиль ветра WindModel до/после (ветер / U10 над рельефом)
До: α 0,14, предел 1,8 — все часы одинаково: 2 м 0,798; 10 м 1; 50 м 1,253; 100 м 1,380; 300 м 1,610.
| условия | класс | 2 м | 10 м | 50 м | 100 м | 300 м | 300 м при U10, м/с (до → после) |
|---|---|---|---|---|---|---|---|
| 9:00 ясно 3 м/с | C | 0,773 | 1 | 1,294 | 1,445 | 1,625 | 4,8 → 4,9 |
| 12, 15 ясно 3 м/с | B | 0,835 | 1 | 1,198 | 1,294 | 1,404 | 4,8 → 4,2 |
| 9–20 облачно 3 м/с; 20:00 ясно 3 м/с | D | 0,680 | 1 | 1,471 | 1,738 | 2,071 | 4,8 → 6,2 |
| любой час до 20:00, 6 м/с | D | 0,680 | 1 | 1,471 | 1,738 | 2,262 | 9,7 → 13,6 |
| 20:30 облачно 3 м/с | E | 0,407 | 1 | 2,138 | 2,138 | 2,138 | 4,8 → 6,4 (v4: 16,4) |
| 20:30 ясно 3 м/с | F | 0,243 | 1 | 1,971 | 1,971 | 1,971 | 4,8 → 5,9 (v4: 43,3) |
У земли (ниже 10 м) ветер днём почти прежний (B: 0,835 против 0,798), в нейтраль — на 15 % слабее (0,680).
4. E/F: насыщение по устойчивому слою (C2 v5)
Классы E/F появляются только вечером после ≈ 20:25 (солнце < 8°): в ясную ночь F до 6 узлов (3,3 м/с), E — 7–10 узлов
(до 5,4 м/с); облачно — F до 3 узлов (1,8 м/с), E — 4–6 узлов. В игре это полёт со старта 20:00 и ход часов до 21:00
(time.max_hour); сам старт 20:00 — C/D. При 6 м/с класс D в любой час.
v4 (первый вариант Б2): z_sat — толщина нейтрального слоя; F давал на 300 м 14,4 × U10 (43 м/с при 3 м/с), E — 5,5 ×. Нефизично: ночной устойчивый слой много тоньше нейтрального.
v5 (решение К2): z_sat = 0,3·h, h = min(0,3 u*/f, 0,4 √(u*·L/f)) для E, F (Зилитинкевич; как air._bl_depth и
AirCase._closure), для D и неустойчивых — 0,3 u*/f (как rules.py). L — Golder 1972: 1/L = a + b·lg z0 (A −0,096/0,029,
B −0,037/0,029, C −0,002/0,018, D 0/0, E 0,004/−0,018, F 0,035/−0,036; Myrup & Ranzieri 1976, Seinfeld & Pandis). При
z0 0,1 м: L(E) = 45,5 м, L(F) = 14,1 м. Сигнатура max_profile(alpha, u10, z0, f_cor, cls) (по умолчанию D), то же в
wind_prof.py; класс передают apply_to_case, WindModel, for_hour.
| класс, U10 | z_sat, м | max_profile | 300 м, м/с |
|---|---|---|---|
| E, 1 / 3 / 5 м/с | 22,4 / 38,9 / 50,2 | 1,572 / 2,138 / 2,467 | 1,6 / 6,4 / 12,3 |
| F, 1 / 3 / 5 м/с | 12,5 / 21,6 / 27,9 | 1,216 / 1,971 / 2,468 | 1,2 / 5,9 / 12,3 |
На высоте ветер теперь ≈ нейтральному (D при 3 м/с — 2,07 × U10), сдвиг сосредоточен в нижних 20–40 м. Оговорка: в штиль F даёт z_sat 8,8 м < 10 м (max_profile 0,896) — при U10 ≲ 0,7 м/с ветер на 10 м в WindModel чуть меньше U10; ветер там ≈ 0, на полёт не влияет. Дневные случаи (A–D) не изменились — фикстуры и bench не пересчитывались (проверено прогоном GPU-тестов, § 6).
5. Цена на сетках игры до/после
Замеры — tools/research/b2/bench.sh (логи out/bench/{base,b2}_*.log, сводка out/cost.md, cost.py). «До» — копия
на базовом коммите dd29bd4 (git worktree), «после» — e674c49; один GPU (RTX 4070 SUPER), по очереди под замком. Монитор
был погашен (DPMS): vsync X11 давал ~1 кадр/с, и первый замер через tools/gpu_tests.sh упёрся в таймауты 60 с —
отброшен; оба замера сделаны с --disable-vsync --max-fps 60 (как монитор 60 Гц). Итерации детерминированы, стена — шумная.
Bench test_air_picard_bench (решение GPU, итераций (эталон f32); GPU, с). Цена итерации не изменилась:
400 м 4,03 мс (без θ′_d 3,18), 200 м 14,2 мс (11,1).
| сетка | ветер, нагрев | до | после |
|---|---|---|---|
| 400 м | штиль, с нагревом | 150 (161); 0,68 | 200 (201); 0,90 |
| 400 м | 3 м/с, с нагревом | 130 (131); 0,60 | 190 (191); 0,85 |
| 400 м | 3 м/с, пара (без + с) | 280 (151 + 131); 1,16 | 420 (231 + 191); 1,69 |
| 400 м | 6 м/с, с нагревом | 130 (131); 0,60 | 130 (131); 0,60 |
| 200 м | штиль, с нагревом | 250 (251); 3,85 (стена 6,9 с) | 1100 (1101); 16,6 (стена 25,3 с) |
| 200 м | 3 м/с, с нагревом | 170 (171); 2,66 | 210 (201); 3,26 |
| 200 м | 3 м/с, пара | 370 (191 + 171); 5,13 | 430 (221 + 201); 5,97 |
| 200 м | 6 м/с, с нагревом | 180 (181); 2,81 | 190 (191); 2,96 |
| 400 м, тёплый старт | 12:00 холодный / тот же тёплым / 12:30 холодный / от 12:00 | 130 / 20 / 120 / 50 | 190 / 20 / 180 / 70 |
Игра (AirRuntime, GPU-тесты test_air_, Онгудай 12:00, 3 м/с, 150°):
| что | до | после |
|---|---|---|
| загрузка, область 400 м | 3,08 с, итер. [150, 130] | 3,66 с, [230, 190] |
| загрузка с окнами («Рассчитываем ветер») | 5,54 с стены; окна 100 м [40, 40], 50 м [50, 60] | 6,18 с (+12 %); [40, 40], [50, 50] |
| итераций цепочки 400 → 100 → 50 (без + с нагревом) | 470 | 600 (+28 %) |
| пересчёт по сроку 12:15, тёплый | GPU 1,15 с, [80, 130] | GPU 1,53 с, [120, 190] |
| пересчёт 12:15 с окнами | 6,04 с | 6,38 с |
| смена ветра 3 → 4 м/с, тёплый | GPU 1,28 с, [110, 120] | GPU 1,38 с, [120, 130] |
Откуда рост (split.py → out/split.md, эталон air.py f32, итераций):
| область | ветер | нагрев | до | только профиль | только λ/h | после |
|---|---|---|---|---|---|---|
| 400 м | штиль | да | 161 | 161 | 201 | 201 |
| 400 м | 3 м/с | да | 131 | 171 | 141 | 191 |
| 400 м | 3 м/с | нет | 151 | 221 | 161 | 231 |
| 200 м | штиль | да | 251 | 251 | 1101 | 1101 |
| 200 м | 3 м/с | да | 171 | 181 | 181 | 201 |
Штиль дорожает от λ/h (×4,4 на 200 м — как в Б1: 1101 против 261), ветер — от профиля: в полдень при 3 м/с (класс B) ветер на высоте 1,40 × U10 вместо 1,8 × — поле «слабее», псевдовремя релаксации то же, нужно больше итераций. Ожидание Б1 «цепочка игры +14 %» — для штиля с нагревом при прежнем α; цепочка игры при 3 м/с — +28 % итераций, +12 % стены на загрузке.
6. Что изменилось в тестах и фикстурах
tests/contracts/test_air_contracts.gd::test_c2_params_match_reference— инвариант v4: общие ключиParams()=AirCase.p;Params().alpha=AirCase.p.alpha= α_N = конфиг =WindProfile.alpha_n(); старых ключей нет; z_sat/max_profile = числаrules.pyна 4 контрольных входах; 8 строк таблицы Тёрнера; WindModel и случай решателя на 12:00, 3 м/с дают одно α и предел. Контракты 26/26.test_air_place— α и max_profiledomain_case400 и 200 м, 0/3/6 м/с = профили эталона из фикстурpicard/.test_weather_day::test_wind_profile_follows_hour— WindModel в атмосфере берёт α/предел из WindProfile по часу погоды; 20:00 круче полудня.test_air_picard_gpu,test_air_window_gpu— профиль ветра случая из фикстуры (profiles),TestAirPicard.set_profile.- Фикстуры пересчитаны теми же генераторами (
tools/research/b2/regen.sh), в JSON —params(lam, lam_frac, pr_t, k_relax) иprofilesпо ветру (alpha, max_profile, класс, высота солнца). Итерации эталона (f32), до → после:
| фикстура | случай | до | после |
|---|---|---|---|
| picard 400 м | штиль с нагревом / без | 161 / 11 | 201 / 11 |
| 3 м/с с нагревом / без | 131 / 151 | 191 / 231 | |
| 6 м/с с нагревом / без | 131 / 131 | 131 / 121 | |
| picard 200 м | штиль с нагревом / без | 251 / 11 | 1101 / 11 |
| 3 м/с с нагревом / без | 171 / 191 | 201 / 221 | |
| 6 м/с с нагревом / без | 181 / 181 | 191 / 181 | |
| window 100 м | 3 м/с с нагревом / без | 41 / 41 | 41 / 41 |
| 6 м/с с нагревом / без | 51 / 51 | 51 / 41 | |
| window 50 м | 3 м/с с нагревом / без | 61 / 51 | 51 / 51 |
| ref | flat_wind / agnesi / heated_slope / saddle | 10 / 710 / 60 / 320 | 30 / 780 / 60 / 470 |
field/, thermals/, blocks/ не пересчитывались: это входы проверок чтения поля и блоков, от Params не зависят.
Прогоны тестов (после, e674c49 + тест хода дня):
- контракты
--filter=test_air_contracts— 26/26; - GPU
tools/research/b2/bench.sh(=gpu_tests.sh --filter=test_air_+--disable-vsync --max-fps 60) — 58/58 (до — тоже 58/58); - headless все папки (
headless.sh, по 8 процессов): core 5, audio 12, contracts 26, instruments 47, net 28, game 116, world 11, tasks 37, vegetation 21, world_objects 31 — без падений; flight 80 — 1, atmosphere 124 (17 GPU пропущено) — 2, ui 78 — 1, terrain 63 — 1, stability 1 — 1:test_takeoff::test_headwind_launch(atlas, 4 м/с — срыв nose_high) иstability/test_matrix(Аушкуль, training, strong — «взлёт не провален») — падают и на базе (копия dd29bd4), не регрессия;test_net_screen— порт занят (port_busy),test_surface::test_forest_mask_loaded— порог времени под нагрузкой (0,61 с против 0,5) — известные, не регрессии;- сдвиг от профиля (на базе зелёные):
test_lee_rotor::test_lee_sink_and_rotor_scale_with_wind— 8 м/с: σ ротора 5,42 при u 11,52 (0,47·u, порог 0,5·u); на базе 5,02 при 9,88 (0,51·u). Класс D (α 0,24, предел 2,45 против 0,14 и 1,8) — ветер в подветренных точках на 20–80 м сильнее на 17 %, σ аналитического ротора выросла на 8 %, отношение упало.test_wave::test_synthetic_ridge_lens_matches_lift— подъём в центре линзы 3,96 при пороге 4,00 (половина max_ms): ветер на опорной высоте волны другой. Пороги — эвристик подветренной зоны и волн (до поля), подобраны под прежний профиль. Решение К2: у порогов нет источника (качественные проверки) — порог по факту с запасом: ротор ≥ 0,45·u, линза ≥ 0,45·max_ms, с комментарием «профиль притока откалиброван (Б2), эвристика не подгоняется»; коэффициенты эвристик не менялись. Обе — зелёные.
После C2 v5: контракты 26/26 (контрольные E/F в тесте), GPU test_air_ — 58/58, headless test_lee_rotor,
test_wave, test_air_place, test_weather_day, test_atmosphere, test_determinism, test_air_model_atmo,
test_wind_field — зелёные.
ТКЭ Askervein с λ/h 0,0158 (решение К2 п. 2)
Метод AM-09б (askervein_fields.py → turb_askervein.gd → fit_s3.py), поля 12,5 / 25 / 50 м с профилем притока Б1
(α 0,242, z0 0,05, max_profile по z_sat) и λ/h 0,0158 — tools/research/b2/tke_recheck.py, out/tke/ (≈ 1,3 мин GPU).
| AM-09б (λ/h 0,25, α 0,17) | Б2 (λ/h 0,0158, α 0,242) | |
|---|---|---|
| χ² ТКЭ/ТКЭ(RS), пороги AM-08, σ данных ⊕ σ сетки (13 точек) | 33,0 | 254,8 |
| по группам: наветренная / вершина / подветренная | 20,0 / 10,3 / 2,8 | 183,0 / 41,0 / 30,7 |
| χ² только с σ данных | 4297 | 3889 |
| медиана модель/данные | 2,68 | 3,12 |
Рост χ² — в основном от σ сетки: с λ = 40 м поля 12,5 и 25 м ближе друг к другу, |Δ| меньше (ASW10: ± 0,08), и прежнее
завышение ТКЭ моделью (модель/данные ≈ 2–3 на наветренной стороне и вершине, и раньше) перестало прятаться в σ сетки.
С одной σ данных χ² даже чуть меньше (4297 → 3889). Завышение ТКЭ над разогнанным склоном — прежняя граница модели
(нет быстрого искажения, docs/guide/air-model.md → «Болтанка над вершиной и на наветренном склоне завышена»); общий
источник λ у масштаба 3 оставлен (решение К2).
7. Вопросы координатору — решены (К2, feature/air-model 3cd6a0c)
- E/F — вариант (а), C2 v5 (§ 4). 2. λ масштаба 3 — общий источник; ТКЭ перепроверена (§ 6). 3. Пороги
test_lee_rotor,test_wave— по факту с запасом (источника нет). 4–5. Кромка «ниже 7000 футов» и ночь по солнцу < 8° — приняты, строки вdocs/guide/air-model.md→ «Границы модели». 6.pilot_check.py— подпись исправлена (am09 — явное 0,25).
GPU test_air_ после v5 — 58/58 (out/bench/b2v5_gpu_tests.log); итерации дневных случаев те же (загрузка с окнами [230, 190], окна [40, 40], [50, 50]) — bench не повторялся.