Решатель 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>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
# 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 уже записан в галилей-инвариантной форме и принимает
|
||||
скорость стенки на линке, но подача этой скорости не подключена;
|
||||
- несколько тел одновременно (сейчас — одно тело на выбор из семи форм).
|
||||
Reference in New Issue
Block a user