Пристеночная функция Spalding, многоуровневый домен, группы J и K

Решатель:
- --wall-function spalding поверх bouzidi/grad/hrr: моментные схемы берут
  модельную скорость узла и u_τ²/ν в тензор давлений, Bouzidi — скольжение
  стенки, согласованное по напряжению (в вязком подслое тождественно ноль);
- --levels: цепочка вложенных уровней с рекурсивным шагом на CPU и GPU,
  одиночный патч — её частный случай, прежние постановки побитово те же;
- --sponge-side, β по узлам; в сводке y⁺, cl_mean, rho_probe_rms.
- Исправлено: на GPU при --refine >= 3 сила лишних подшагов терялась за
  краем буфера, Cd занижался в r/2 раза.

Кампания: группы J (65, схемы стенки с функцией) и K (25, внешний домен),
205 прогонов на 5 долей по ≈27 ч; bench/compare.py сводит J и K против
эталонов; parity.py сверяет Spalding и три уровня. Образ 1.4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 12:25:43 +03:00
co-authored by Claude Opus 5.5
parent 1d495e3e89
commit 86e9c7139d
14 changed files with 5574 additions and 596 deletions
+124 -11
View File
@@ -311,8 +311,11 @@ c_s²(1/(γβ) − ½) и, поскольку измеренная ⟨γ⟩ ≈
низкими числами Рейнольдса, поскольку на границе возникают паразитные скачки») и естественная
форма для подвижных стенок: скорость стенки входит в целевые значения, а не отдельной поправкой.
В здешней канальной постановке преимущества по устойчивости воспроизвести не удалось: при росте
Re обе модели теряют счёт на одном и том же значении (Re ≈ 5·10⁴ при теле в 16 клеток), то есть
ограничивает не стенка, а что-то другое — вероятнее всего Zou–He при τ → ½.
Re обе модели теряют счёт на одном и том же значении, то есть ограничивает не стенка, а
что-то другое — вероятнее всего Zou–He при τ → ½. (Сама оценка «Re ≈ 5·10⁴ при теле в 16
клеток» была сделана по коротким прогонам; длинные дают предел τ − ½ ≈ 6·10⁻⁴, то есть при
D = 16 уже Re ≈ 4000, — см. раздел «Дальше». Вывод «стенка не ограничивает» он не меняет:
Bouzidi и HRR со Spalding разваливаются при одних и тех же Re.)
**Умолчание — `hrr`, и это решение временное.** Оно принято по устройству схемы (третий порядок
Эрмита фильтрует высокочастотный мусор у стенки, чего обрыв на Π не делает), а не по здешним
@@ -326,6 +329,59 @@ Re обе модели теряют счёт на одном и том же зн
это только потому, что две модели дали побитово одинаковый результат там, где обязаны были
разойтись.
### Пристеночная функция Spalding
Ключ `--wall-function none|spalding` (умолчание `none` — всё прежнее не меняется побитово)
ставит поверх `bouzidi`, `grad` или `hrr` закон стенки Spalding (1961): одна формула на весь
пристеночный слой, от вязкого подслоя (u⁺ = y⁺) до логарифмической зоны, κ = 0.41, B = 5.2.
С `staircase` не сочетается: ступенька не знает, где стенка.
Как устроено:
- у каждого граничного узла есть нормаль (градиент SDF), расстояние до стенки y_w и точка
отбора скорости на одну клетку дальше по нормали (Malaspinas, Sagaut 2014); скорость там —
билинейно по полю на момент t. Геометрия строится один раз в `cpu::Geom` и загружается на
GPU как есть;
- по касательной скорости в точке отбора Ньютоном находится u_τ, по нему — модельная скорость
узла u_node = u_τ·u⁺(y⁺_w) и напряжение τ_w = ρu_τ²;
- **`grad`/`hrr`**: касательная составляющая целевой скорости (B 1) заменяется на u_node, а в
тензоре давлений нормальная производная касательной скорости — на u_τ²/ν. Плотность (B 3) и
сборка популяций не меняются;
- **`bouzidi`**: стенка получает скорость скольжения u_s = u_node − u_τ²·y_w/ν и входит в
отскок поправкой подвижной стенки. Скольжение согласовано по **напряжению**: отскок передаёт
стенке ρν(u_node − u_s)/y_w = τ_w. В вязком подслое u⁺ = y⁺, и u_s тождественно ноль; в
буферной и логарифмической зонах u_s < 0 — стенка «отступает», добирая трение до τ_w.
Модуль скольжения зажат скоростью в точке отбора.
Первая версия согласовывала скольжение по **скорости** (прямая через стенку, узел и точку
отбора). В решателе без турбулентной вязкости это даёт большое положительное скольжение —
стенка становится почти свободной: цилиндр Re = 10⁴, D = 16 выдал Cd = 0.016. Вторая брала
разрешённую скорость узла вместо модельной и при Re = 20 расходилась с прилипанием на 2 %:
у искривлённой стенки узел не лежит на прямой от стенки к точке отбора. Нынешняя формула
свободна от обоих изъянов.
**Сведение к прилипанию при малом y⁺** (цилиндр Re = 20, D = 16, 320×192, y⁺ ≈ 0.25, GPU):
| модель | прилипание | Spalding | разница |
|---|---|---|---|
| `bouzidi` | 2.38252 | 2.38252 | 0 (до 7-го знака) |
| `grad` | 2.38774 | 2.38989 | +0.09 % |
| `hrr` | 2.38835 | 2.39053 | +0.09 % |
У моментных схем остаточные 0.09 % — от того, что целевая скорость строится теперь по нормали
к стенке, а не усреднением по линкам (B 1).
**Закон стенки описывает турбулентный слой, а при умеренных Re он ламинарный.** На цилиндре при
Re = 2000 (D = 16, y⁺ ≈ 5, местами до 19, 16 тыс. шагов) функция заметно двигает только Bouzidi:
Cd 2.10 против 1.56 без неё; у `grad` 1.59 против 1.65, у `hrr` 1.70 против 1.69. Bouzidi
навязывает модельное трение через обмен импульсом жёстко, моментные схемы — через тензор
давлений одного слоя узлов, мягче. Какая схема ближе к эталону — меряет группа J кампании.
Каждый прогон канала пишет в сводку y⁺ первого узла на самом тонком уровне (`yplus_mean`,
`yplus_max`, долю узлов с y⁺ > 30 `yplus_frac_log`, `u_tau_mean`) — два десятка снимков во
второй половине прогона, с функцией и без неё. Без y⁺ сравнение схем стенки не
интерпретировать.
### Паритет бэкендов и согласованность уровней
CPU (f64) и GPU (f32) на одной постановке совпадают до 4–5 значащих цифр шаг в шаг: ⟨ρ⟩
@@ -335,6 +391,18 @@ CPU (f64) и GPU (f32) на одной постановке совпадают
Один и тот же случай с патчем ×2 и вовсе без измельчения (`--refine 1`) даёт St 0.1951 против
0.1970 и ⟨Cd⟩ 1.956 против 1.943 — связка уровней систематики не вносит.
**Исправлено: сила на GPU при `--refine 3` и выше.** Рабочие слоты силы в буфере итогов были
рассчитаны ровно на два подшага тонкого уровня; сила третьего и следующих писалась за край
буфера и терялась, а делилась сумма всё равно на r. Cd на GPU выходил занижен в r/2 раза:
два тела при `--refine 3` — 1.316 против 1.972 на CPU. Теперь слотов столько, сколько подшагов
самого тонкого уровня на шаг L0: 1.9733 против 1.9716. Это затрагивало прогон `F09_refine3`
кампании и любой ручной запуск с `--refine ≥ 3` на GPU; при `--refine 2` и без патча
результат прежний побитово.
`bench/parity.py` сверяет CPU и GPU на четырёх случаях: Тейлор–Грин, стационарный цилиндр,
цилиндр с `grad` + Spalding и три уровня ×2×2. Замерено на Iris Xe (Vulkan): Cd совпадает до
1.7·10⁻⁵ (Spalding) и 7.3·10⁻⁵ (три уровня), y⁺ — до 6.4·10⁻⁵.
**Предел на размер сетки снят.** У GPU есть жёсткий предел `maxComputeWorkgroupsPerDimension`
= 65535, а диспетчеризация была одномерной: при 64 узлах на рабочую группу это упирало сетку в
4.2 миллиона узлов, то есть примерно 2048×2048. Всё, что крупнее, падало ошибкой валидации —
@@ -470,15 +538,56 @@ rms Cl 0.81 вместо 0.40 — то есть 75-процентная прод
просто переносится во времени: поток всё равно обязан разогнаться вокруг тела, и энергия этого
переходного процесса задана физикой, а не гладкостью начального поля. Умолчание — 0.
## Многоуровневый домен
`--levels "r:up,down,side; r:up,down,side; …"` строит цепочку вложенных уровней вместо одного
патча: L0 — самый грубый, на весь домен, каждый следующий в r раз тоньше родителя и лежит
внутри него. `up`, `down`, `side` — отступы границ уровня от центра тела в его диаметрах
(вверх по потоку, вниз, в стороны). Так строится внешний домен с грубыми буферами: большой и
дешёвый L0, к телу сетка мельчает ступенями. `--levels` перекрывает `--refine`/`--patch`;
одиночный патч — частный случай цепочки из одного звена, и прежние постановки дают **побитово
тот же** результат на обоих бэкендах (сверено на пяти постановках: без патча, ×2, ×3, два тела,
периодический случай; единственное исключение — исправленная сила при `--refine 3` на GPU).
```sh
# L0 в 4 раза грубее тела; ступени ×2: 4D вверх, 15D вниз, ±4D, затем 1.5D, 10D следа, ±2D
./target/release/kbc2d --shape cylinder --size 8 --nx 480 --ny 320 --body-x 160 --re 150 \
--levels "2:4,15,4; 2:1.5,10,2" --wall bouzidi --steps 96000 --backend gpu
```
Важно: `--size`, `--nx`, `--ny`, `--steps` и Re задаются **в клетках и шагах L0**. Тело в 8
клеток L0 при цепочке ×2×2 — это 32 клетки на самом тонком уровне; отчёт печатает D и τ
каждого уровня. τ растёт к тонким уровням (τ_k = r_k(τ_{k−1} − ½) + ½, ν одна на всех), так
что ближе всего к пределу устойчивости τ → ½ именно L0.
Шаг рекурсивный: уровень делает шаг, затем его потомок — r подшагов с временной интерполяцией
рамки, затем рестрикция обратно; сила снимается на самом тонком уровне на каждом его подшаге.
Стенки канала, вход, выход и губки есть только у L0. На GPU это та же рекурсия в одном
командном буфере; ядра связки не менялись.
Проверено: однородный поток без тела — неподвижная точка через три уровня с точностью 10⁻¹²;
стационарный цилиндр Re = 20 при одном скачке ×4 и при цепочке ×2×2 с тем же разрешением тела
даёт Cd 2.2104 и 2.2156 (0.24 %); CPU и GPU на трёх уровнях совпадают до 7.3·10⁻⁵ по Cd.
**Боковые губки** `--sponge-side N` — N рядов L0 у верхней и нижней стенок с той же плавной
надбавкой вязкости, что у входа и выхода; β теперь поле по узлам, а не по столбцам. Нужны,
чтобы отличать отражения от боковых стенок от отражений на стыках уровней.
В сводку добавлены `levels` (цепочка с размерами), `nodes_per_step` (обновлений узлов за шаг L0
по всем уровням), `wall_function`, `sponge_side`, `rho_probe_rms` (пульсация плотности в зонде
во второй половине — мера паразитной акустики) и `cl_mean`; в `series.csv` — столбец
`rho_probe`.
## Валидационная кампания
`bench/` — 115 прогонов на ≈90 машинных часов, разложенных по девяти группам: эталоны
`bench/` — 205 прогонов на ≈101 машинный час, разложенных по одиннадцати группам: эталоны
первоисточников, цилиндр против литературы, модели стенки, профили крыла, сложная и
множественная геометрия, границы домена, старт и время жизни, внутренние инварианты,
сверхмелкие сетки до 4096×2048. Из них 108 идут на GPU (≈87 часов) и 7 — на CPU (≈3 часа,
исследования сходимости группы A). Каждый прогон кладёт в собственную папку `cmd.txt`,
`log.txt`, `report.txt`, `series.csv` и `summary.json`; гифку на всю свою длительность
пишут **59 прогонов из 115**, остальным `--gif` не передаётся вовсе.
сверхмелкие сетки до 4096×2048, схемы стенки с пристеночной функцией (J) и внешний домен с
грубыми буферами (K). Из них 198 идут на GPU и 7 — на CPU (≈3 часа, исследования сходимости
группы A). Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`,
`series.csv` и `summary.json`; гифку на всю свою длительность пишут **59 прогонов из 205** —
группы J и K гифок не пишут. Итоги J и K сводит `bench/compare.py`.
```sh
cd bench
@@ -497,7 +606,7 @@ python preflight.py # каждый сценарий старту
## Развёртывание (Docker)
Собранный образ опубликован: **`notbigghost/kbc2d:1.3.0`** (он же `latest`, платформа `linux/amd64`).
Собранный образ опубликован: **`notbigghost/kbc2d:1.4.0`** (он же `latest`, платформа `linux/amd64`).
Исходники на сервере не нужны — достаточно перенести туда один файл
`docker-compose.server.yml`:
@@ -525,7 +634,7 @@ docker run --rm --gpus all kbc2d --calibrate
### Пять долей на пяти машинах
Кампания разложена на **пять равных по времени долей** (≈24 ч каждая, поле `shard` в
Кампания разложена на **пять равных по времени долей** (≈27 ч каждая, поле `shard` в
`scenarios.json`), чтобы считать её одновременно на пяти чужих ПК с видеокартой под Windows
или WSL2. На машину переносятся два файла — `docker-compose.shards.yml` и `shard.bat` — и
выполняется `shard.bat 3` (или `docker compose -f docker-compose.shards.yml up -d shard3`).
@@ -618,7 +727,11 @@ GPU не найден. Согласие даётся явно, переменн
стартовый импульс от появления тела в потоке;
- подвижные и вращающиеся тела: GMEM уже записан в галилей-инвариантной форме и принимает
скорость стенки на линке, но подача этой скорости не подключена;
- разбор нерешённых 12% по Cd и предела устойчивости Re ≈ 5·10⁴ — обе задачи вынесены в
кампанию (группы B и G), выводы делать по её данным;
- разбор нерешённых 12% по Cd и предела устойчивости — обе задачи вынесены в кампанию
(группы B и G), выводы делать по её данным. Оценка предела Re ≈ 5·10⁴ при D = 16 была сделана
по коротким прогонам и не подтвердилась: в длинных (цилиндр 20D×12D, Bouzidi) Re = 10⁴
разваливается при D = 16 и 32 на 13 и 27 тысячах шагов, а держится при τ − ½ ≥ 6·10⁻⁴
(Re = 2000 при D = 8, 5000 при D = 32, 10⁴ при D = 64); пристеночная функция предел не
сдвигает;
- больше четырёх тел в домене: сейчас сила считается по четырём вёдрам (`MAX_BODY_BUCKETS`),
геометрия при этом собирается из любого числа тел, но силы сверх четвёртого сливаются вместе.