Files
CFDManager/docs/theory/2d_solver/README.md
T
NotBigGhostandClaude Opus 5 5d762ad9d6 Решатель KBC-2D на Rust: ядро по статьям, два бэкенда, гифка в реальном времени
Переписанный с нуля двумерный решатель LBM D2Q9 с энтропийным столкновением KBC
в варианте «модель D» (табл. I 2D-статьи Bösch/Chikatamarla/Karlin, arXiv:1507.02509;
в трёхмерных работах — KBC-N1). Код разложен по ролям на пять файлов: математика
решателя, бэкенд под процессор, бэкенд под видеокарту, оркестратор, блок гифок.

Ядро сверено с первоисточниками тестами (22 шт.):
* проектор на сдвиг, выписанный аналитически из представления популяций через
  натуральные моменты (ур. 10), совпадает с матричным до 1e-13, идемпотентен;
* γ из замкнутой оценки (ур. 17) — корень условия максимума энтропии (ур. 15);
* сдвиговые моменты релаксируют ровно с 2β при любой γ, вязкость по ур. (5)
  воспроизводится затуханием сдвиговой волны с точностью лучше 1%;
* сквозной бенчмарк статьи (дважды периодический сдвиговый слой, Re=3e4) сходится
  с fp64-эталоном питоновского решателя 0.6035.

Порог вырожденности γ относительный (доля от ⟨Δ|Δ⟩): абсолютный подменял бы γ на 2
на большинстве узлов, молча превращая KBC в LBGK. Доля таких узлов печатается в отчёте.

Бэкенды взаимозаменяемы и согласованы: CPU (rayon, f64) и GPU (wgpu/WGSL, f32) на одной
постановке совпадают до 4–5 значащих цифр шаг в шаг; на Intel Iris Xe GPU даёт ~105 MLUPS
против ~18 у процессора. Топология задачи строится один раз в cpu.rs и загружается в
буферы, дублируется только физика — в WGSL.

Анимация привязана к физическому времени потока, а не к скорости счёта: задержка кадра
берётся из δt = u_lat·δx/u_phys. Дробная задержка раскладывается по целым сотым долям
секунды накопителем (3,3,4,3,3,4,…), поэтому накопленное время кадров не уходит от
физического; режим --gif-every auto подбирает шаг под реальное время при заданной частоте.

Параметризовано: скорость и направление потока, число Рейнольдса, размер домена и размер
ячейки в метрах, коэффициент и границы вложенного патча измельчения, семь форм тела
(цилиндр, квадрат, ромб, эллипс, профиль NACA, треугольник, пластина) с углом атаки,
время и разгон, оператор столкновения, режим выхода и губка, бэкенд, вся анимация и
три уровня подробности отчёта.

Известное расхождение с питоновским решателем на канальном случае (Cd выше на 9%,
St ниже на 12% при совпадающих ⟨ρ⟩, ⟨Cm⟩ и rms Cl) описано в README вместе с тем,
что уже исключено как причина.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:23:03 +03:00

227 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# kbc2d — двумерный решатель LBM D2Q9 с энтропийным столкновением KBC
Переписанный на Rust решатель обтекания тела в канале. Физика — та же, что в
`docs/theory/solver_2x_sdf` (python/CuPy), но собранная заново: с тестами против формул
первоисточников, двумя взаимозаменяемыми бэкендами и анимацией, привязанной к физическому
времени потока.
Модель столкновения — **KBC D** по таблице I работы Bösch, Chikatamarla, Karlin, *Entropic
Multi-Relaxation Models for Simulation of Fluid Turbulence* (arXiv:1507.02509); в трёхмерных
работах тех же авторов она называется **KBC-N1**. Сдвиговая часть `s` несёт натуральные
моменты {N, Π_xy}, всё остальное (T, Q_xyy, Q_yxx, A) уходит в `h`. Статьи лежат в
`docs/origins/`; ссылки на формулы в коде даны по нумерации 2D-статьи.
## Устройство
Пять файлов по ролям — каждый отвечает ровно за одно:
| файл | роль |
|---|---|
| `src/math.rs` | **математика решателя.** Решётка D2Q9, энтропийное равновесие в product-form, проектор на сдвиг, стабилизатор γ, столкновение, Zou–He, геометрия тел через SDF, перевод единиц, спектральная диагностика. Всё узловое и чистое; здесь же тесты против формул статьи. |
| `src/cpu.rs` | **бэкенд под процессор.** Раскладка AoS, обход сетки, rayon, сборка Bouzidi-линков, связка уровней AMR. Физику берёт из `math`. |
| `src/gpu.rs` | **бэкенд под видеокарту.** wgpu + WGSL (Vulkan/DX12/Metal), раскладка SoA. Физика построчно повторяет `math.rs` на f32; топологию (маски, линки, рамка патча) не дублирует, а берёт из `cpu`. |
| `src/main.rs` | **запуск и оркестрирование.** Разбор параметров, сборка постановки, цикл по шагам, живой вывод и итоговый отчёт, выгрузка рядов в CSV. Здесь же контракт `Spec` / `StepRec` / `FieldKind`, общий для обоих бэкендов. |
| `src/gif.rs` | **создание гифок.** Тайминг относительно физического времени, палитры, нормировка, служебная надпись, кодирование. |
## Сборка и запуск
Нужен Rust 1.75+.
```sh
cargo build --release # с GPU-бэкендом
cargo build --release --no-default-features # только CPU (без wgpu)
cargo test --release # 21 быстрый тест
cargo test --release -- --ignored # плюс эталонный сдвиговый слой (~3 с)
```
Пример: цилиндр Re=150, гифка завихренности в реальном времени.
```sh
./target/release/kbc2d --shape cylinder --size 24 --re 150 \
--nx 480 --ny 240 --steps 40000 --sponge-len 32 \
--gif wake.gif --gif-field vorticity --verbose full
```
`--help` показывает все ключи, разбитые по группам: Физика, Сетка, Тело, Время, Схема,
Анимация, Вывод.
## Синхронизация анимации с физическим временем
Требование: гифка идёт с той же скоростью, что и настоящий поток, независимо от того, с какой
скоростью считает машина. Реальная производительность в тайминг не входит вообще.
В LBM скорость самой решётки жёстко равна c = δx/δt = 1, поэтому шаг по времени однозначно
определяется тем, какую **решёточную** скорость `u_lat` мы назначаем физическому потоку:
```
δt = u_lat · δx / u_phys [с/шаг]
шагов в секунду = u_phys / (u_lat · δx)
задержка кадра = (шагов на кадр) · δt / playback
```
**Про пример из постановки.** «30 м/с, ячейка 0.1 м ⇒ 300 шагов/с» — это арифметика
δt = δx/u_phys, то есть `u_lat = 1`: поток проходит ровно ячейку за шаг. Формула
воспроизводится буквально ключом `--u-lat 1.0` (тест `units_time_scaling` это проверяет), но
физически такой режим негоден: Ma = u_lat/c_s = √3 ≈ 1.73, сверхзвук, разложение
Чепмена–Энскога не работает. Поэтому по умолчанию `u_lat = 0.05` (Ma ≈ 0.087), и те же 30 м/с
при ячейке 0.1 м дают 6000 шагов/с. Синхронность гифки выдерживается в обоих случаях — меняется
только, сколько шагов приходится на кадр.
**Две тонкости формата GIF**, обе разобраны:
1. *Задержка хранится в сотых долях секунды.* Точная физическая задержка почти никогда не целая:
30 кадр/с — это 3⅓ сотых. Покадровое округление до 3 дало бы анимацию на 11% быстрее
реальности, и уход копился бы линейно (к тысячному кадру — 3.3 секунды). Поэтому задержки
выдаёт накопитель `DelayDither`: суммарное время кадров отслеживает точное физическое с
точностью до одной сотой (3, 3, 4, 3, 3, 4, …), ошибка ограничена ±5 мс и не растёт.
2. *Меньше одной сотой не бывает.* При физически корректном `u_lat` кадр каждые 10 шагов — это
600 кадр/с, чего формат не умеет. Поэтому режим по умолчанию `--gif-every auto` подбирает
шаг сам под `--gif-fps` (30) так, чтобы получилось ровно реальное время. Если шаг задан
жёстко и задержка не представима, программа печатает фактический коэффициент расхождения и
конкретный совет, как починить.
`--gif-speed 0.1` даёт замедление в 10 раз (тоже точно, через тот же накопитель).
## Что параметризовано
- **поток**: скорость (м/с), направление, число Рейнольдса, решёточная скорость (число Маха);
- **сетка**: размеры домена и размер ячейки в метрах (плотность сетки), коэффициент измельчения
вложенного патча и его границы;
- **тело**: семь форм на выбор — `cylinder`, `square`, `diamond`, `ellipse`, `naca`, `triangle`,
`plate` — плюс характерный размер, относительная толщина, угол атаки и положение;
- **время**: число шагов, длина разгона, амплитуда и длительность стартового возмущения;
- **схема**: оператор столкновения (`kbc`/`bgk`), режим выхода, поглощающая губка, бэкенд,
число потоков;
- **анимация**: файл, поле (`speed`/`vorticity`/`density`/`gamma`), палитра, масштаб, шаг кадра,
частота, скорость воспроизведения, диапазон нормировки;
- **вывод**: период живых строк, три уровня подробности, число окон в отчёте о сходимости, CSV.
## Что печатает отчёт
*Шапка* — вся постановка с производными величинами: δt, шагов на секунду, Ma, τ, физическая
вязкость, блокировка канала, геометрия патча, полный план тайминга анимации.
*Живой вывод* — шаг, физическое время, ⟨ρ⟩, max|u|, Cd, Cl, скорость счёта и ETA; на уровне
`full` дополнительно ⟨γ⟩ с размахом, доля вырожденных узлов, доля узлов с ξ < 0, MLUPS и
отношение скорости счёта к реальному времени.
*Итог* — установившийся режим (St, ⟨Cd⟩, rms Cl, ⟨Cm⟩) сырой и с поправкой на блокировку, рядом
литературные значения для цилиндра; таблица сходимости по окнам с вердиктом о дрейфе массы и
насыщении; разбор стабилизатора γ; производительность.
## Состояние проверки
**Ядро схемы проверено против формул статей** (`cargo test`, 21 тест + 1 длинный):
- проектор Δs, выписанный аналитически из представления популяций через натуральные моменты
(ур. 10), совпадает с матричным `M⁻¹·diag(…,1,1,…)·M` до 1e-13; он идемпотентен и не несёт
ни массы, ни импульса;
- равновесие в product-form сохраняет ρ и ρu до 1e-13;
- γ из замкнутой оценки (ур. 17) — корень условия критической точки энтропии (ур. 15): невязка
при γ\* более чем в 20 раз меньше, чем при γ\*±1;
- при γ = 2 схема совпадает с LBGK поточечно;
- **сдвиговые моменты релаксируют ровно с 2β при любой γ** (проверено при β = 0.3, 0.6, 0.95) —
это и есть гарантия того, что стабилизатор не трогает вязкость;
- затухание сдвиговой волны даёт ν из ур. (5) с погрешностью < 1% при τ = 0.6 и τ = 1.0;
- Zou–He ставит ровно заданные скорость на входе и плотность на выходе;
- SDF всех семи форм: знак верен внутри и снаружи, |∇φ| = 1 ± 0.05 на контрольном кольце.
**Сквозная сверка с эталоном.** Дважды периодический сдвиговый слой — один из трёх бенчмарков
2D-статьи (N=128, Re=30000, u₀=0.04, κ=80, δ=0.05, одно конвективное время). Отношение
энстрофии к начальной сходится с fp64-эталоном питоновского решателя **0.6035**. Тест
чувствителен именно к тому, что важно: на испорченном (абсолютном) пороге вырожденности γ тот
же прогон давал 0.6599, то есть +9.3%, а чистый LBGK при этих параметрах разваливается.
**Паритет бэкендов.** CPU (f64) и GPU (f32) на одной постановке совпадают до 4–5 значащих
цифр шаг в шаг: ⟨ρ⟩ 1.04933 против 1.04934, Cd 2.339 против 2.338, ⟨γ⟩ 1.2645 против 1.2646.
На Intel Iris Xe GPU даёт ≈82 MLUPS против ≈18 MLUPS у процессора.
**Согласованность уровней AMR.** Один и тот же случай, посчитанный с патчем ×2 и вовсе без
измельчения (`--refine 1`), даёт St 0.1951 против 0.1970 и ⟨Cd⟩ 1.956 против 1.943 — расхождение
в пределах 1–3%. То есть связка уровней (рамка, подшаги, рестрикция) не вносит систематики.
**Формы тел ведут себя физично.** Один короткий прогон на каждую форму (260×130, размер 20,
угол атаки 12°) даёт ожидаемый порядок сопротивления: обтекаемые — профиль 0.44, эллипс 0.45,
пластина 0.60; промежуточные — цилиндр 1.33, ромб 1.65; тупые — квадрат 2.38, треугольник 2.72.
Момент ⟨Cm⟩ при этом ≈ 0 **только** у круга (−0.0002), которому угол атаки безразличен, а у
несимметричных под углом тел он ненулевой (профиль +0.31, эллипс +0.15, пластина +0.13) —
то есть и плечо, и знак момента считаются осмысленно.
### Сверка канального случая с питоновским решателем
Постановка ровно та же, что в `solver_2x_sdf` по умолчанию: 174×90, D=16, cx=40, Re=150,
U=0.07, патч ×2 на [16,140]×[13,77], жёсткий ноль поперечной скорости на выходе, 100 000 шагов.
Слева — что даёт этот решатель, справа — что записано в README питоновского.
| величина | kbc2d | solver_2x_sdf | |
|---|---|---|---|
| ⟨ρ⟩ | 1.001 | 1.001 | совпало |
| ⟨Cm⟩ | +0.00001 | −0.00003 | оба ≈ 0 — симметрия считывания в порядке |
| rms Cl (с попр.) | 0.571 | 0.557 | расхождение 2.5% |
| ⟨Cd⟩ (сырой) | 1.956 | 1.799 | **на 9% выше** |
| St·(1−β) | 0.161 | 0.183 | **на 12% ниже** |
Расхождение по Cd и St не объяснено. Что про него известно:
- оно **не** от связки уровней: без измельчения картина та же (см. выше);
- оно **не** от ядра схемы: сдвиговый слой из статьи воспроизводится с эталоном, вязкость точна
до 1%, сдвиговые моменты релаксируют ровно с 2β;
- после поправки на блокировку **этот** Cd = 1.32 ближе к литературным 1.33 (0.6%), чем
питоновский 1.22 (8.6% ниже), а вот питоновский St ближе к литературным 0.183;
- rms Cl у обоих решателей вдвое выше литературной ~0.3 — это известная особенность постановки,
разобранная в `solver_2x_sdf/README.md` (продольные границы завышают давленческие амплитуды).
Развести это можно только прогоном обоих кодов бок о бок на одной машине; питоновская версия
требует GPU и CuPy, которых на машине сборки нет, поэтому сравнение сделано с числами,
записанными в её README.
### Известные отличия от питоновского решателя
- **Внутри тела столкновение не считается.** Эти популяции фиктивны: Bouzidi перекрывает всё,
что могло бы прийти из тела в жидкость, так что на физику они не влияют. Питоновская версия
считает их наравне со всеми. Побочный эффект — статистика γ здесь собирается строго по
жидкости.
- **Рестрикция дополнительно пропускает узлы, у которых тонкий узел-источник лежит внутри
тела.** В питоновской версии маска строится только по грубому уровню. Случай краевой (тело на
обоих уровнях одно и то же), но здесь он закрыт явно: из фиктивного узла в жидкий переносить
нечего.
- **Точность.** Процессорный бэкенд работает в f64 (питоновский по умолчанию в f32,
переключается переменной `AMR_FP64`); GPU-бэкенд — в f32, потому что в WGSL нет двойной
точности.
### Порог вырожденности γ
`GREL = 1e-8` — **относительный** порог, доля от ⟨Δ|Δ⟩, а не абсолютный. Это принципиально:
знаменатель ⟨Δh|Δh⟩ квадратичен по неравновесию и физически мал (~1e-7…1e-9 в развитом следе),
поэтому абсолютный порог срабатывает на подавляющем большинстве узлов и молча подменяет γ на 2,
то есть гонит чистый LBGK вместо KBC. Доля вырожденных узлов печатается в отчёте — на исправном
пороге она обязана быть ~0.
Единственное исключение — самый первый шаг: поле в точности равно равновесию, Δ ≡ 0, и порог
честно срабатывает на всех узлах. На γ это не влияет, потому что она умножается на Δh = 0.
## Акустика канала
Пара «вход по скорости / выход по давлению» — недодемпфированный акустический резонатор:
затухание продольной моды идёт как ν(π/Nx)², то есть на длинном домене её почти ничто не гасит.
Это разобрано в факторном исследовании питоновского решателя (`solver_2x_sdf/README.md`,
эксперимент №1): длинные домены без демпфера дают смещённый режим ⟨ρ⟩ ≈ 1.66 или развал счёта.
Отсюда два решения в умолчаниях:
- `--outlet extrapolate` стоит по умолчанию (в том исследовании — лучший вариант по всем
метрикам: жёсткий ноль поперечной скорости отражает вихри дорожки обратно к телу);
- при `nx ≥ 250` и выключенной губке программа печатает предупреждение с готовым рецептом
(`--sponge-len 32`).
Мгновенное ⟨ρ⟩ при этом всё равно осциллирует вокруг единицы — это сама акустическая мода.
Смотреть надо на **оконные средние** в таблице сходимости: именно они должны стоять на 1.000.
## Дальше
- σ·n-кросс-чек силы (интеграл тензора напряжений по контуру) как независимая проверка GMEM —
в питоновской версии есть, здесь пока нет;
- подвижные и вращающиеся тела: GMEM уже записан в галилей-инвариантной форме и принимает
скорость стенки на линке, но подача этой скорости не подключена;
- несколько тел одновременно (сейчас — одно тело на выбор из семи форм).