А1 — структурные правки решателя воздуха: план (А1.1)
Задание для исполнителя кода (А1.2, dp-engineer) и основа ревью (А1.3). Источник задач — отбор Морриса
(docs/research/air-model-sensitivity.md; журнал docs/archive/plan/air-model-progress.md → «Итог отбора Морриса»).
Пробы «сейчас / после» без правок решателя — tools/research/a1/ (подклассы PrtAir, SplitAir,
HAir в probe.py; числа в out/*.json; воспроизведение — README.md там же). Строки кода — по
feature/air-model на 30.09.2026 (28ab3de).
Коротко
| Правка | Что не так сейчас | Что делаем | Где |
|---|---|---|---|
| 1. Pr_t | Params.pr_t объявлен, но khf = nuf (air.py:701) — K_θ ≡ K_m; в GPU параметра нет | K_θ = K_m/Pr_t, K_θ,h = K_h/Pr_t; Pr_t — параметр обоих решателей (Params.pr_t, AirCase.p.pr_t, слот P_IPRT) | air.py ядро build_heat; air_picard.glsl:heat; air_case.gd |
| 2. τ только для диабатического θ′ | −θ′/τ гасит и адиабатические θ′ = −γ·δz (устойчивое обтекание): седловина без нагрева зависит от τ (×1,71 / 1,90 / 1,99 при τ 1800 / 7200 / 21600 с — проба) | второй переносимый скаляр θ′_d (диабатическая часть): L θ′_d = Q − θ′_d/τ; полное θ′: L θ′ = Q − w γ − θ′_d/τ (схема A′). Проба: ×2,051 / 2,051 / 2,055 — от τ не зависит (Δ = 0,004 < 0,05) | оба решателя: ядро тепла (два прохода), новые буферы thd, thbd, ореол окна от родителя, тёплый старт, невязка, баланс тепла |
3. closure = const + heat_mode = cbl | _closure при const не считает h → AttributeError: 'Air' object has no attribute 'h_bl' (проба) | толщина слоя h, w*, L — всегда (Троен–Март), при const постоянна только вязкость | только air.py (_closure); в GPU closure нет и не нужен |
Решения на шлюз пользователя (раздел 6): значение Pr_t (рекомендация 0,85; варианты 1,0 / 0,74 / 0,95); смысл и значение τ (оставить 7200 с, смысл сужается до диабатической части); следствия правки 2 — седловина уходит от ×1,9 к ×2,05 при цели пилота 1,5 ± 0,2 (физика модели при Fr ≈ 2, не подгонять) и подъём у старта в штиль Онгудай 12:00 ниже на 15 % (0,52 → 0,44 м/с в окне 50 м; при ветре без изменений): крышка слоя перемешивания перестаёт «протекать» через τ.
Контракты: C1 → v2 (схема, reference.md, фикстуры ref/, picard/, window/ — пересчёт), C2 → v3
(AirCase.p.pr_t, тёплый старт с thd), C7 → v2 (parent_data/window_state с thd, θ′_d ореола);
C3, C4 без изменений (канал theta = полное θ′). Подробно — раздел 5.
Объём для исполнителя: ≈ 1,5–2 дня работы (air.py 0,5; GPU 0,5–1; фикстуры, тесты, документы 0,5).
1. Правка 1 — турбулентное число Прандтля
Физика
Уравнение тепла модели (reference.md → «Уравнения»):
∇·(u θ′) + w dθ̄/dz = ∇·(K_θ ∇θ′) + Q − θ′/τ − s_θ (θ′ − θ_b), сейчас K_θ = K_m (Pr_t = 1).
K-теория первого порядка: K_θ = K_m/Pr_t. Измерения нейтрального приземного слоя дают Pr_t < 1:
- Businger, Wyngaard, Izumi, Bradley (1971) JAS 28:181 — Pr_t = 0,74 при κ = 0,35; пересчёт на κ = 0,40 (Högström 1988 BLM 42:55) даёт ≈ 0,85–0,95;
- Högström (1996) BLM 78:215 — обзор: нейтрально φ_h(0) ≈ 0,95 ± 0,05 при φ_m(0) = 1, т. е. Pr_t ≈ 0,95;
- Kays (1994) J. Heat Transfer 116:284 «Turbulent Prandtl number — where are we?» — для сдвиговых течений вдали от стенки 0,85 (инженерный стандарт);
- Li (2019) Atmos. Res. 216:86 — обзор для АПС: нейтрально 0,7–1,0, при неустойчивости падает (0,5–0,7 у земли), при устойчивости растёт (> 1 при Ri ≳ 0,2);
- K-профильные схемы с нелокальным членом (Troen & Mahrt 1986 BLM 37:129; Holtslag & Boville 1993 J. Climate 6:1825; YSU — Hong, Noh, Dudhia 2006 MWR 134:2318) задают Pr ≈ 1 в конвективном слое: часть потока тепла несёт противоградиентный член, и Pr_t локальной диффузии поднимают, чтобы не считать его дважды. В нашей модели нелокальный перенос — распределение источника Q по слою (heat_mode = cbl), не противоградиентный поток, поэтому этот довод к K_θ ниже z_i применим лишь частично.
Варианты на шлюз:
| Pr_t | Довод за | Довод против |
|---|---|---|
| 0,85 (рекомендация) | измеренное нейтральное значение (Kays 1994; середина 0,74–0,95 Businger/Högström); K_θ работает в нейтрали, в устойчивом воздухе над z_i и в механическом слое — там Pr_t < 1 измерен | в конвективном слое K-профильные схемы держат Pr ≈ 1 |
| 1,0 | как Троен–Март / HB93 / YSU при нелокальном члене; статус-кво, фикстуры от этого пункта не меняются | измерения нейтрали дают меньше; параметр «для галочки» |
| 0,74 / 0,95 | классика Businger 1971 / Högström 1996 при κ = 0,40 (наше κ) | края диапазона |
Зависимость от устойчивости (Pr_t(Ri)) не вводим: устойчивость уже в F(Ri) местного K; при cbl конвективный перенос — нелокальный источник. Записать в «Границы модели».
Насколько это меняет ответ. Проба probe.py prt (Онгудай 12:00, цепочка 400 → 100 → 50 м, air.py):
| U10 | Pr_t | K_θ max, 400 м | подъём у старта, 400 м / окно 50 м | ветер 50 м над стартом | θ′ 50 м над стартом |
|---|---|---|---|---|---|
| 0 | 1,0 | 862 | 0,395 / 0,516 | 0,80 | 0,72 |
| 0 | 0,85 | 963 | 0,382 / 0,505 | 0,75 | 0,72 |
| 0 | 0,74 | 1058 | 0,370 / 0,496 | 0,71 | 0,71 |
| 3 | 1,0 | 1025 | 1,199 / 1,332 | 5,06 | 0,32 |
| 3 | 0,85 | 1150 | 1,197 / 1,328 | 5,05 | 0,31 |
| 3 | 0,74 | 1268 | 1,195 / 1,326 | 5,04 | 0,31 |
При cbl эффект мал: 0,01–0,02 м/с по подъёму (Моррис давал 0,1–0,17, но при перемешанном плане, где
Pr_t сильнее всего работал вместе с heat_mode = surface). Выбирать Pr_t по литературе, не по подъёму.
Дискретная форма
В шаблоне тепла для соседа q по оси d: dif = ½(𝒦[idx] + 𝒦[q])/h² · (1/Pr_t), 𝒦 = K по z, K_h по x, y.
Один множитель 1/Pr_t на все три оси (одно определение K_θ = K/Pr_t; горизонтальная часть K_h в слое
перемешивания и так равна K, где преобладает Смагоринский — численная подсеточная диффузия, множитель
безвреден). Замечание: MAir Морриса делил только вертикальную часть; расхождение второго порядка.
Места
air.py
Params.pr_t(стр. 510) — есть; комментарий: «турбулентное число Прандтля, K_θ = K_m/Pr_t (Kays 1994: 0,85)»; значение по решению шлюза.- Ядро CUDA
build_heat(стр. 232–266): аргументreal inv_prt; стр. 259:dif = 0.5*(KK[idx]+KK[q])*ih*ih*inv_prt. Air.__init__стр. 701: убрать псевдонимself.khf = self.nuf; завестиself.inv_prt = 1/prm.pr_t.Air.build_heat(стр. 964–971): передаватьself.nuf, self.nuh, …, R(self.inv_prt).Air.heat_budget(стр. 1141):K = self.nuf.get()*self.inv_prtвместоself.khf.- reference.md: строки 98, 135, 199, 263 («Pr_t = 1») → K_θ = K/Pr_t, значение и ссылка.
GPU
air_picard.glslстр. 38–43:const int P_IPRT = 20;(слот 19 занятP_NEST).air_picard.glsl:heatстр. 434–435:float kd = (d == 2 ? nu[idx] + nu[q] : nuh[idx] + nuh[q]) * prm[P_IPRT];.air_case.gd:p.pr_tв словареp(стр. 45–71, рядом сtau_cool); послеprm.resize(32)(стр. 398):prm[P_IPRT] = 1.0 / float(p.pr_t); константыP_NEST,P_IPRT— вAirCase(сейчасP_NESTобъявлен вair_window_case.gd:15).- Проверка: блок «шаблон тепла C» теста
test_air_picard_gpuпротив фикстурref/, посчитанных с Pr_t ≠ 1 (см. раздел 4, шаг 1).
2. Правка 2 — выхолаживание только диабатической части θ′
Физика
θ′ в модели — отклонение от фона θ̄(z) погоды на час. У него два источника:
- адиабатический −w dθ̄/dz: устойчиво стратифицированный воздух, поднятый/опущенный над рельефом, холоднее/теплее фона на θ′_a = −γ·δz. Это консервативная величина (потенциальная температура частицы не меняется), она и есть механизм блокировки, обтекания в обход, волн. Её единственный физический сток — турбулентное перемешивание (уже в ∇·(K_θ∇θ′)) и радиационное выхолаживание с масштабом суток (Newtonian cooling ≈ 1–2 К/сутки, τ_rad ~ 10⁵ с), на масштабе области (10 км / 5 м/с = 30 мин) пренебрежимое;
- диабатический Q — нагрев от поверхности. Стационарной задаче нужен сток: без него в штиль θ′ растёт неограниченно. Физически сток — то, что фон θ̄(z, t) погоды уже включает средний прогрев слоя за день: правильный источник — Q − ⟨Q⟩ (отклонение от среднего по области прогрева), а −θ′_d/τ — его замена релаксацией; при типовом ⟨Q⟩ ≈ H̄/(ρc_p z_i) ≈ 300/(1206·1500) ≈ 1,7·10⁻⁴ К/с и θ′_d ~ 1 К τ = θ′_d/⟨Q⟩ ≈ 6000 с, т. е. τ = 7200 с согласуется с этим смыслом.
Сейчас −θ′/τ действует на сумму и за 7200 с (при 5 м/с — 36 км пути!) съедает адиабатическое охлаждение поднятого воздуха: блокировка слабеет, поток легче переваливает хребет, ×(седло/склон) зависит от τ (Моррис S = 3,3; проба ниже). Правка: релаксация только θ′_d.
τ после правки. Смысл: время, за которое диабатическая аномалия растворяется в среднем прогреве слоя (перемешивание с фоном, чей прогрев уже в θ̄). Значение 7200 с оставить (Моррис: диапазон 1800–21600 с двигает подъём у старта на 0,2 м/с — калибровать позже по LES, план п. 5). Вариант для шлюза: заменить релаксацию на Q − ⟨Q⟩ по слою (без τ) — физичнее, но это другая постановка (стационарность в штиль без выноса не гарантирована), не в А1.
Схема разделения (выбор: A′ — «полное θ′ + диабатическое θ′_d»)
Оператор L θ = ∇·(u θ) − ∇·(K_θ ∇θ) линеен при замороженных u, K (Пикар). Решаем два скаляра:
L θ′_d = Q − θ′_d/τ − s_θ (θ′_d − θ_b,d) (диабатическая часть)
L θ′ = Q − w dθ̄/dz − θ′_d/τ − s_θ (θ′ − θ_b) (полное θ′; τ входит явным источником)
Разность θ′ − θ′_d = θ′_a удовлетворяет L θ′_a = −w γ − s_θ(θ′_a − θ_b,a) — адиабатическая часть без τ.
Полное θ′ остаётся основным полем: плавучесть в build_mom, N² в kloc, ореол окна thb, тёплый старт
th, канал theta игры — всё без изменений. θ′_d — вспомогательный скаляр (буфер thd, ореол thbd).
Почему не иначе:
- Разложение без второго скаляра невозможно. θ′_a = −γ·δz — нелокальный функционал траекторий (интеграл −wγ вдоль линии тока); в стационарном эйлеровом решателе его не выразить через поля в клетке. Любой «локальный» признак (знак θ′, наличие Q в столбце, высота) ошибается там, где важно — поднятый холодный воздух над прогретым склоном.
- Вариант A (θ′_a и θ′_d, θ′ = сумма) эквивалентен, но требует суммы в
build_mom,kloc, ореоле, канале игры, тёплом старте — больше правок и буферов в чтении; A′ трогает только шаг тепла. - τ → ∞ (сутки, радиация) — в штиль стока нет, стационарного решения нет (потому τ и появился).
- Q − ⟨Q⟩ без τ — другая постановка, отложено (выше).
Численно: в уравнении полного θ′ пропадает 1/τ из диагонали (диагональное преобладание чуть меньше, адиабатическая часть теряет искусственный сток) — проба: седловина сошлась за 161–181 итераций против 101–171; Онгудай — см. таблицу ниже. Источник −θ′_d/τ берётся от θ′_d этой итерации (порядок: собрать шаблон θ′_d → прогонки θ′_d → собрать шаблон θ′ → прогонки θ′).
Полунеявная плавучесть cplz = couple·g/θ0·max(γ_w,0)·s_th, s_th = Δτ_θ/(1 + Δτ_θ/τ) (air.py:675,
air_case.gd:268): s_th — эффективная постоянная времени уравнения θ′ на псевдошаге; после правки у полного
θ′ нет 1/τ, согласованное значение s_th = Δτ_θ (1200 с вместо 1029 с). Неподвижная точка от cplz не
зависит (член и в диагонали, и в правой части), это численный коэффициент; меняем ради согласованности,
фикстуры (cplz, Kz, проводимости V-цикла) пересчитываются вместе со всем.
Поведение
- Границы области («жёсткие»): θ′ и θ′_d в ореоле = 0; губка s_θ (потолок и приточные грани) тянет оба к 0. Адиабатическое θ′ на притоке 0 — воздух не возмущён; у потолка — гашение волн, как и было.
- Окна (C7): ореол и цель зоны релаксации — от родителя, оба скаляра:
thb← θ′ родителя,thbd← θ′_d родителя (трилинейно, как сейчас θ′). Решение без нагрева: Q = 0 → θ′_d ≡ 0 (родитель без нагрева даёт нулевой θ′_d;parent_data.mech.thd— нули). - Тёплый старт:
state()/warm/window_state()несутthd; старого состояния безthdне бывает (совместимость не нужна), но безопасно: нет ключа → 0 (θ′_d сходится с нуля). - Сдвиг окна (
air_window:shift):thdпереносится из старого окна так же, какth. meta()/ поле игры (C3, C4): каналtheta= полное θ′ — термикам (AirThermals: θ̄ + θ′ для потолка частицы) и болтанке (T_N2) нужна полная стратификация. θ′_d в игру не отдаём (термики читаютheatнапрямую); возможный будущий канал — вне А1.- Решение без нагрева: θ′_d ≡ 0 точно (Q = 0, ореол 0, старт 0). Второй проход тепла там можно
пропустить, но программы GPU (
_progs) общие для пары случаев — в А1 считать всегда (простота); оптимизация — ключ программы с признаком нагрева (по желанию, если замер шага покажет > +15 %). - Баланс тепла (
heat_budget, тест GPUheat_budget): выхолаживание = Σ θ′_d/τ (не θ′).
Проба «сейчас / после» (probe.py saddle, седловина Морриса: хребет 500 м, седло 250 м, 100 м, 5 м/с
вдоль оси, N = 0,01, без нагрева; probe.py ongudai)
| τ, с | сейчас ×20 м | сейчас ×50 м | сейчас за седлом 50 м | после ×20 м | после ×50 м | после за седлом | итераций сейчас / после |
|---|---|---|---|---|---|---|---|
| 1800 | 1,708 | 1,586 | 0,93 | 2,051 | 1,867 | 1,29 | 101 / 161 |
| 7200 | 1,900 | 1,744 | 1,14 | 2,051 | 1,867 | 1,29 | 151 / 171 |
| 21600 | 1,992 | 1,820 | 1,24 | 2,055 | 1,871 | 1,30 | 171 / 181 |
| Δ(1800 − 21600) | −0,284 | −0,233 | −0,004 | −0,004 |
Условие приёмки (2) «Δ < 0,05» прототип выполняет. θ′_d ≡ 0 в этом случае (проверено), θ′_min = −1,39 К (адиабатическое охлаждение поднятого воздуха; было −1,18…−1,36 в зависимости от τ). Следствие: седловина уходит к ×2,05 при цели пилота 1,5 ± 0,2 — искусственный сток маскировал физику модели (Fr ≈ 2, поток переваливает хребет, reference.md → развилка 2); решать на калибровке по устойчивости дня, не τ.
Онгудай 12:00 (probe.py ongudai, цепочка 400 → 100 → 50 м, Pr_t = 1, air.py float32):
| U10 | итераций 400 / 100 / 50 м | подъём у старта 400 м / окно 50 м | w и ветер на 50 м над стартом | θ′ 50 м над стартом | θ′_d / θ′_a диапазон (400 м) | |
|---|---|---|---|---|---|---|
| 0 | сейчас | 61 / 71 / 161 | 0,395 / 0,516 | 0,19 / 0,80 | 0,72 | — |
| 0 | после | 151 / 71 / 151 | 0,253 / 0,441 | 0,05 / 0,53 | 0,58 | 0…0,98 / −0,98…+0,72 |
| 3 | сейчас | 91 / 41 / 71 | 1,199 / 1,332 | 0,72 / 5,06 | 0,32 | — |
| 3 | после | 131 / 41 / 71 | 1,202 / 1,336 | 0,72 / 5,09 | 0,32 | 0…0,52 / −0,37…+1,44 |
Все статусы ok. При ветре разницы нет (адиабатические θ′ выносятся потоком). В штиль подъём у старта
падает на 15 % (окно 50 м: 0,52 → 0,44 м/с; область 400 м: 0,39 → 0,25): термики над склоном
перехлёстывают верх слоя перемешивания и в устойчивом слое над z_i охлаждаются адиабатически на ~1 К;
раньше τ за 2 ч стирал это охлаждение (и адиабатический нагрев опускания, +0,7 К), крышка слоя
«протекала», средний подъём тянулся выше. Теперь крышка работает как в физике (зона вовлечения — предел
термика), но это заметное пилоту изменение (0,08 м/с в окне при пороге 0,1) — на шлюз (раздел 6, п. 5).
Сходимость: область в штиль 151 итерация против 61 (1,5 с против 0,6 с на 4070 SUPER) — второй скаляр и
отсутствие стока 1/τ у полного θ′; окно 50 м не хуже. Баланс тепла rel — в out/ongudai.json.
Буферы и параметры (имена, единицы, раскладка)
air.py (Air) | GPU (AirPicardJob.buf, air_picard.glsl) | Что | |
|---|---|---|---|
| θ′ полное | th (есть) | th (есть) | К, N с ореолом, клетки |
| θ′_d | thd — новый cp.zeros(shape) | thd — новый gpu.buffer(n) | К, N с ореолом; 0 в земле и вне cell = 1 (как th) |
| цель губки / ореол θ′_d | thbd — новый (0; окно — из родителя в set_nest_bc) | thbd — новый (0; окно — air_window:nest) | К, N |
| шаблон θ′_d | Ctd (7, N), btd (N), ctd (N, поправка 2-го порядка) | тот же Cu (8N: C0..C6, b) по очереди: сначала θ′_d, потом θ′ | — |
| Q_eff = Q − θ′_d/τ | Qeff (N), считается перед сборкой θ′ | в ядре: qsrc[idx] − thd[idx]·prm[P_ITAU] при pc.i0.w == 1 | К/с |
| 1/Pr_t | inv_prt (float), аргумент ядра build_heat | prm[P_IPRT = 20] | — |
| τ | prm.tau_cool → inv_tau ядра: 1/τ для θ′_d, 0 для θ′ | prm[P_ITAU] — только при pc.i0.w == 0; при 1 — 0 | 1/с |
| dθ̄/dz | gam для θ′, gam0 = zeros(NZ) для θ′_d | lev[L_GAM] при pc.i0.w == 1, 0 при 0 | К/м |
| решение без нагрева | — | thdm (копия thd в _rec_copy_wmech) | для state(true), parent_data.mech |
| тёплый старт / состояние | state() → +thd; init_from(st) → thd = st.get("thd", 0) | warm.thd, state() → thd/thdm, parent_data.heat/mech.thd, window_state() | N |
| окно: родитель | set_nest_bc: трилинейно P.thd (маска cell ≠ 0) → thbd, thd ореола | par_thd (буфер родителя), ядро nest → thbd; old_thd → ядро shift | N родителя / старого окна |
| невязка θ′_d | residuals() → thd_rms, thd_max; критерий th_rms = max(θ′, θ′_d) | _resid(Cu, thd, 3) в слоты S_R2D = 22, S_RMAXD = 23 (AirGpu.SCALARS = 64, 20–21 — S_NET окна); _read_residuals — max двух | К/с |
| cplz | s_th = prm.dtau_th (стр. 675) | s_th = dth (air_case.gd:268) | с |
Ядро build_heat/heat в дискретном виде (клетка типа 1), режим m ∈ {0 — θ′_d, 1 — θ′}:
x = m ? θ′ : θ′_d; diag = 1/Δτ_θ + (m ? 0 : 1/τ) + s_θ
b = x/Δτ_θ + (m ? Q − θ′_d/τ − γ_k·½(w[k] + w[k+1]) : Q) + s_θ·(m ? θ_b : θ_b,d) − corr2(x)
по 6 граням: перенос против потока как сейчас; сосед не земля → dif = ½(𝒦[idx] + 𝒦[q])/h²/Pr_t,
diag += dif, C_q −= dif (𝒦 = K по z, K_h по x, y)
не-неизвестная клетка: C = (1, 0…), b = x
Порядок в итерации (п. 7 reference.md → «Итерация Пикара»): шаблон θ′_d → heat_sweeps × (z, x, y) для
θ′_d → шаблон θ′ (с новым θ′_d) → heat_sweeps × (z, x, y) для θ′. Невязка (residuals/_rec_check):
шаблон θ′_d → r_d; шаблон θ′ → r; критерий по max.
3. Правка 3 — толщина слоя h при closure = const
Air._closure (air.py:734–788) при const возвращает на стр. 748–749, не задав self.h_bl, self.wstar;
дальше __init__ читает self.h_bl для нагрева cbl (стр. 667) → AttributeError (проба probe.py const);
λ = max(λ, λ/h·h) при const молча падает на λ = 40 м (стр. 694: hasattr). Смысл: h — свойство слоя
(Троен–Март: z_i − h_s, 0,3 u*/f, Зилитинкевич), не замыкания; постоянная вязкость — только вид K(z).
Правка (только air.py): разделить _closure на _bl_depth() (u*, H_s, h_s, h, w*, L, unst — стр. 750–771,
всегда) и профиль K (стр. 772–785 при hb; np.full(shape, nu_const) при const); closure_info при const —
dict(kind="const", nu=nu_const, h_max=…). Убрать hasattr на стр. 694. Проба (HAir, heated_slope
fixtures.py, штиль): const + cbl считается, 51 итерация, h_max 1348 м, w_max 0,69 м/с (hb: 41 итерация,
0,60 м/с). В GPU closure/nu_const/adv2/limiter — исследовательские ручки, в AirCase.p их нет и не
добавляем (записать в C2).
tools/research/morris/model.py: класс MAir (Pr_t и h при const) становится лишним — удалить, оставив
A.Air (в docstring отметить: Моррис считался с Pr_t только по вертикали). recal/run_grid.py импортирует
model ради params, GpuLock, free, meta — работает без MAir.
4. Порядок правок и проверки на каждом шаге
Каждый шаг — отдельный коммит; после шага — указанная проверка. Godot — только с XDG_DATA_HOME=$(mktemp -d);
GPU-пробы под flock /tmp/heat_ca_gpu.lock; venv с CuPy и brotli — tools/research/morris/README.md.
Шаг 0 — числа «до». tools/research/a1/out/{saddle,prt,const,heat,ongudai}.json уже есть (этот план);
для Askervein — tools/research/recal/out/runs_check25.jsonl → χ² 68,06 (a1/askervein_chi2.py).
Шаг 1 — Pr_t в air.py. Ядро build_heat + inv_prt, Params.pr_t = значение шлюза, heat_budget,
reference.md. Проверка: probe.py prt с A.Air (без PrtAir) даёт числа таблицы раздела 1 (совпадение
с PrtAir до 1e-4 м/с); Askervein run_grid.py check25 не меняется (нет нагрева, θ′ ≡ 0).
Шаг 2 — Pr_t в GPU. P_IPRT, p.pr_t, ядро heat. Проверка: пересчитать ref/ (fixtures.py) с
pr_t шлюза (и для heated_slope явно ≠ 1, чтобы блок был чувствителен), test_air_picard_gpu
блоки ≤ 1e-5.
Шаг 3 — h при const (air.py). _bl_depth. Проверка: probe.py const без HAir → const+cbl ok; hb-случаи
побитно прежние (ref/ не меняются: сравнить .bin до/после).
Шаг 4 — разделение θ′ в air.py. Массивы thd, thbd, Ctd/btd/ctd, Qeff; build_heat двухпроходный
(θ′_d: gam = 0, inv_tau = 1/τ, ореол thbd; θ′: Q_eff = Q − θ′_d/τ, inv_tau = 0), heat_step, residuals
(критерий по обоим), set_ghosts_background, init_*, state, set_nest_bc (θ′_d ореола), heat_budget,
adv2_heat для θ′_d при adv2; s_th = dtau_th. Образец — SplitAir в a1/probe.py (там всё это как
подкласс; в air.py — прямо в Air, одним ядром build_heat с параметрами). Проверка: probe.py saddle
на A.Air даёт «после» из таблицы раздела 2 (Δ < 0,05); probe.py heat — now = split (θ′_a ≡ 0 в слое
перемешивания, отличие ≤ 1e-4 К); probe.py ongudai — таблица раздела 2. Баланс тепла (heat_budget):
Онгудай rel ≤ 2e-3 (проба: 0,3–1,9e-3); седловина без нагрева — rel 7,5e-3 при критерии th_rms 5e-7
и 1,2e-3 при 5e-8 (проверено: bg = Σγw ≈ 0, баланс — губка против выноса, невязка задаётся критерием
остановки, не ошибкой схемы; у «сейчас» rel меньше только потому, что в масштабе стоит большой член
выхолаживания).
Шаг 5 — разделение θ′ в GPU (область). Буферы thd, thbd, thdm; ядро heat с режимом pc.i0.w
(0 — θ′_d, 1 — θ′); bc пишет thd ореола; _rec_heat_step, _rec_check (невязка θ′_d — слоты
S_R2D = 22, S_RMAXD = 23; S_NET = 20, 21 заняты окном; AirGpu.SCALARS = 64 — места хватает;
_read_residuals читает 20 скаляров → 24),
_read_residuals (th_rms = max двух), warm, state, parent_data, _rec_copy_wmech. Проверка:
fixtures.py → новые массивы (in_thd, Ch_d, bh_d, heat_thd, Ch, bh, heat_th, sol_thd) и
блок 6 теста; picard_gpu_refs.py → test_air_picard_gpu решения (≤ 1e-3·u_s, θ′ ≤ 0,05 К), баланс
тепла (cool по thd).
Шаг 6 — окна GPU. air_window:nest (+pthd → thbd), shift (+old_thd → thd), _upload_case
(par_thd, old_thd), window_state. Проверка: window_gpu_refs.py → test_air_window_gpu (поле окна,
граница, баланс, побитная повторяемость, клипмап в рантайме).
Шаг 7 — документы и контракты. reference.md (разделы «Уравнения», «Турбулентная вязкость», «Тепло»,
«Граничные условия», «Итерация Пикара», «Физика», «7. Эталоны»), docs/contracts/air-model.md по
разделу 5 (К1 фиксирует до А1.2, исполнитель правит тесты контрактов в том же коммите), morris/model.py
без MAir, docs/guide/air-model.md/air_model_gpu.md если там описан буфер тепла. Рецепт приёмки — раздел 6.
5. Что меняется в контрактах, фикстурах, тестах, инструментах
C1 → v2 (docs/contracts/air-model.md:38–63): «Дискретизация» — reference.md меняется (уравнение θ′ с
двумя скалярами, K_θ = K/Pr_t, s_th = Δτ_θ, порядок итерации п. 7: «шаблон θ′_d → прогонки → шаблон θ′ →
прогонки», критерий остановки по обоим, баланс тепла); фикстуры ref/ — пересчёт fixtures.py всех четырёх
случаев, новые массивы in_thd, Ch_d (7N), bh_d, heat_thd, sol_thd; params в JSON — с pr_t;
picard/ (picard_gpu_refs.py, ~3 мин) и window/ (window_gpu_refs.py, ~5 мин) — пересчёт (меняются
все решения с нагревом и седловина saddle без нагрева; heat_budget с новым cool). Тесты
test_c1_ref_fixture_format, test_c1_ref_mask_rule, test_c1_ref_solution_div_free — по формату
проходят после пересчёта; test_contract_versions — константы версий.
C2 → v3 (стр. 65–87): AirCase.p + pr_t; запись «closure, nu_const, adv2, limiter —
исследовательские параметры Params, в AirCase.p их нет (GPU: hb, 1-й порядок)»; warm/state() —
{u, v, w, th, thd, p}. meta() без изменений.
C3, C4 — без изменений: theta = полное θ′ (записать явно: «θ′ = θ′_a + θ′_d, канал — сумма»).
C7 → v2 (стр. 217–265): AirPicardJob.parent_data() → heat/mech: {u, v, w, th, thd};
AirWindowJob.window_state() → те же с thd; ореол окна: «θ′ и θ′_d ореола — трилинейно из родителя».
Тест test_c7_window_grid_and_api — если проверяет ключи parent_data/state, добавить thd.
Тесты GPU: test_air_picard_gpu.gd блок 6 (стр. 344–365: два прохода, _heat(0)/_heat(1), массивы
Ch_d/bh_d/heat_thd), heat_budget (стр. 747–823: cool по thd, nu × 1/Pr_t в диффузии через
границу); test_air_window_gpu.gd — heat_budget (стр. 216+) наследует; test_air_picard_bench.gd —
замер шага (ожидание: +20–30 % на шаг тепла, +8–12 % на итерацию; записать в docs/guide/air-model-gpu.md).
test_air_gpu_blocks.gd, test_air_runtime_gpu.gd, test_air_thermals.gd, test_air_place.gd — по смыслу
не трогаются; прогнать.
Инструменты: morris/model.py — убрать MAir (см. раздел 3); recal/run_grid.py — без правок;
tune/askervein_runs.py, synth.py, ref_study.py, real.py — без правок (θ′_d внутри Air);
to_game_field.py/common.py/study.py — прикидка solver.py (мёртвый код с 449f703, в игру и эталон не
входит) — не трогать; docs/research/air-model-sensitivity.md — примечание о правках (после А1.3).
Что может сломаться:
AirGpu.SCALARSменьше 24 → расширить;gpu.download(gpu.scalars, 20)в_read_residuals— читать 24;- размер буфера
Cu(8N) хватает для обоих шаблонов по очереди, но между_heat(0)и_heat(1)идут прогонки θ′_d — порядок записи в одну программу строго последовательный (как сейчас); - окно без нагрева при родителе с
mechбезthd(старыйparent_data) — падение на ключе: делатьsrc.get("thd")→ нули; - fixtures.py
set_state/in_*— добавитьthd, иначе блок θ′ теста стартует не с того состояния; AirRuntime._warmхранитstate()целиком — растёт на 1 буфер (окно 50 м: +1 МБ) — приемлемо;- сходимость: у полного θ′ нет 1/τ — на седловине +10–60 итераций; штиль 12:00 в окне 50 м — см. таблицу Онгудая; если где-то станет «max», это не откатывать, а разобрать в А2 («сходимость в штиль»).
6. Рецепт приёмки А1 и решения на шлюз
Все запуски — из корня копии; PY = venv Морриса; GPU под замком.
| # | Условие | Как измерить | Число / допуск |
|---|---|---|---|
| (1) | Pr_t ≠ 1 меняет K_θ и подъём, одинаково в обоих решателях | air.py: flock … $PY tools/research/a1/probe.py prt (после правки PrtAir можно заменить на A.Air — числа те же) → out/prt.json; GPU: test_air_picard_gpu блок «шаблон тепла C/b» против ref/heated_slope с pr_t ≠ 1 и решение против picard/ | air.py: K_θ max 400 м 862 → 963 (Pr_t 1 → 0,85), подъём в окне 50 м 0,516 → 0,505 (штиль), 1,332 → 1,328 (3 м/с); GPU: блоки ≤ 1e-5 отн., решение ≤ 1e-3·u_s, θ′ ≤ 0,05 К (различие 1 против 0,85 по шаблону — 15 %, блок его видит; по решению при 3 м/с — 0,003 м/с, ниже допуска — поэтому чувствительность даёт блок, не решение) |
| (2) | седловина без нагрева не зависит от τ | flock … $PY tools/research/a1/probe.py saddle (после правки — A.Air и есть «split») | сейчас Δ×20 м = 0,284; после ≤ 0,05 (прототип 0,004) |
| (3) | const + cbl | flock … $PY tools/research/a1/probe.py const | now_const_cbl.status = "ok", итераций ~51, h_max 1348 м |
| (4) | Askervein при перекалибровке | cd tools/research/recal && $PY run_grid.py check25 (без внешнего flock: run_grid.py сам берёт замок /tmp/heat_ca_gpu.lock, внешний даёт взаимоблокировку; уже посчитанную точку он пропускает — поэтому старый out/runs_check25.jsonl сначала отложить, переименовав, а после сверки вернуть), затем $PY ../a1/askervein_chi2.py out/runs_check25.jsonl | χ² 68,06 ± 2 (нейтрально, θ′ ≡ 0, Pr_t не действует; правки не должны его трогать). Группы: наветренная 7,1, вершина 6,4, подветренная 25,4, линия B 6,7, профиль RS 22,5 |
| (5) | тесты | XDG_DATA_HOME=$(mktemp -d) godot --headless --path . res://tests/run_tests.tscn -- --filter=test_air_contracts; то же test_air_picard_gpu, test_air_window_gpu, test_air_runtime_gpu, test_air_gpu_blocks, test_air_thermals | все зелёные; допуски прежние (блоки 1e-5; решение 1e-3·u_s / 0,05 К; окно то же; побитная повторяемость окна) |
Дополнительно: probe.py ongudai до/после (таблица раздела 2) — итерации не хуже ×1,5, статусы ok;
test_air_picard_bench — время итерации (записать).
Решения пользователя (шлюз до А1.2):
- Pr_t: 0,85 (рекомендация) / 1,0 / 0,74 / 0,95 — раздел 1. Эффект на подъём при cbl ≤ 0,02 м/с.
- τ = 7200 с оставить; смысл — релаксация только диабатической части (раздел 2). Альтернатива Q − ⟨Q⟩ — не в А1.
- Принять следствие правки 2: седловина ×2,05 (цель пилота 1,5 ± 0,2, было ×1,9 при τ = 7200) — физика модели при Fr ≈ 2; в «Границы модели»; вопрос устойчивости дня — калибровка, не решатель.
- Pr_t — общий множитель для вертикальной и горизонтальной диффузии тепла (рекомендация) или только
вертикальный (как
MAirМорриса). - Принять следствие правки 2 в штиль: подъём у старта Онгудай 12:00 ниже на 15 % (окно 50 м 0,52 → 0,44 м/с; при ветре без изменений) — крышка слоя перемешивания перестаёт «протекать». Это физика (адиабатическое охлаждение перехлёста термика в устойчивом слое над z_i), не настройка; компенсировать параметрами нельзя — только калибровкой нагрева по LES (план п. 5). Порог заметности пилоту 0,1 м/с — на грани.
7. Открытые вопросы (К1)
- Ускорение штиля на области 400 м (61 → 151 итераций) — приемлемо для загрузки (+0,9 с), но если «Рассчитываем ветер» станет заметен, это вход в А2 (сходимость в штиль), не откат правки.
- Пропуск второго прохода тепла у решения без нагрева — в А1 не делать; решить после замера
bench. - Повторить Моррис (журнал п. 6) — после А1–А3;
MAirудалён, факторыpr_t,tau_coolтеперь настоящие. - α в игре (журнал 30.09) от А1 не зависит.
8. Решения К1 при приёмке плана (30.09.2026)
План принят. Для А1.2:
- Pr_t = 0,85 — решение пользователя 30.09.2026 (окончательно); до решения стояло временно (шлюз п. 1 уходит пользователю через главную сессию, но А1.2 не ждёт: значение — одна константа в
Params.pr_t(air.py) иAirCase.p.pr_t(air_case.gd); фикстуры несутpr_tвparamsJSON, тесты берут его оттуда, поэтому смена значения после решения — две строки и документы, без пересчёта эталонов).heated_slopeвref/— с явным pr_t ≠ 1 (как в шаге 2). - Множитель 1/Pr_t — общий для вертикальной и горизонтальной диффузии тепла (п. 4 шлюза решает К1: одно определение K_θ = K/Pr_t; горизонтальная K_h в слое перемешивания — та же турбулентная вязкость).
- τ = 7200 с оставить, смысл — релаксация только θ′_d (п. 2; в рамках плана, значение не меняется).
- Следствия правки 2 (седловина ×2,05; подъём у старта в штиль −15 %; область в штиль 61 → 151 итераций) — физика модели, не откатывать и не подгонять; в «Границы модели» и в отчёт пользователю (п. 3, 5 — к сведению); сходимость в штиль — А2.
- Контракты C1 v2, C2 v3, C7 v2 зафиксированы К1 до А1.2 (
docs/contracts/air-model.md); исполнитель дописывает контрактные проверки новых ключей (thd) в том же шаге, что код.