Files
CFDManager/docs/theory/2d_solver
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
..

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+.

cargo build --release                       # с GPU-бэкендом
cargo build --release --no-default-features  # только CPU (без wgpu)
cargo test --release                         # 21 быстрый тест
cargo test --release -- --ignored            # плюс эталонный сдвиговый слой (~3 с)

Пример: цилиндр Re=150, гифка завихренности в реальном времени.

./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 уже записан в галилей-инвариантной форме и принимает скорость стенки на линке, но подача этой скорости не подключена;
  • несколько тел одновременно (сейчас — одно тело на выбор из семи форм).