Files
CFDManager/docs/theory/solver_2x_sdf/README.md
T
NotBigGhostandClaude Opus 5 d7881496b0 Сверка документации с кодом: исправлены расхождения
Сплошная проверка документов против дерева. Код не менялся.

docs/rust_vs_cpp.md — числа, снятые с чужого каталога сборки:
- «775 МБ исходников зависимостей» и «10 пакетов в графе C++» получены
  на build/, сконфигурированном из другого дерева (его _deps содержит
  262 МБ nlohmann_json, который проект не объявляет). На деле девять
  объявленных репозиториев дают ~400 МБ исходников;
- пакетов в графе Rust 82, а не 76 (cargo tree по текущему Cargo.lock);
- итог по строкам 2976/494, а не 2962/492: строка «меш» устарела на 17
  строк игнорируемого диагностического теста, тесты в модулях — 176;
- «перевес почти весь в Vulkan-слое» — на деле 346 из 542 (64 %), из них
  305 на Context и Swapchain; остальное приходится на меш, редактор и
  приложение;
- «ни один замер не даёт разницы в порядок величины» неверно:
  инкрементальная release — 52.3 против 4.4 с, это 11.9x;
- граней с пятью и более вершинами в plane.obj 119, а не 121;
- из Catch2 перенесены все три случая, и добавлено ещё два, а не
  «три перенесены дословно»;
- деструктор Renderer::Impl — 22 строки, а не тридцать;
- путевых зависимостей у порта две: assets/meshes с откатом на текущий
  каталог и pipeline_cache.bin рядом с бинарём;
- граф целей CMake ацикличен, цикл существует на уровне исходников —
  именно поэтому CMake и молчит.

rust/README.md:
- объявленный rust-version = "1.82" недостижим: залоченные egui,
  egui-winit и epaint 0.36.1 требуют 1.95;
- перечни зависимостей крейтов были неполны, приведены целиком;
- build.rs читает ../../../shaders, а не ../../shaders;
- тесты не «чистая математика»: тесты загрузчика читают файлы из
  assets/meshes и требуют клон репозитория;
- assets/ порту никто не копирует, он находит корневую копию сам;
- те же поправки про цикл в CMake и про момент ожидания простоя.

README.md:
- панель называется Mesh, а не «Mesh load»;
- пресета clang-cl не существует, компилятор пресеты не фиксируют;
- версии Vulkan SDK и компиляторов сборкой не проверяются;
- Buffer и Image перечислены среди рабочих модулей vk/, хотя ими не
  пользуется никто.

docs/theory/solver_2x_sdf/README.md:
- предлагался несуществующий переключатель cfg.collision="bgk" и реестр
  операторов get; тот же файл двумя разделами ниже говорит, что оператор
  зафиксирован.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:11:06 +03:00

163 lines
17 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.
# solver_2x_sdf — модель цилиндра 2×+SDF, разложенная по компонентам
Самодостаточный LBM-решатель (D2Q9, KBC) для обтекания цилиндра: грубый уровень **L0**
плюс один вложенный патч **L1** (измельчение ×2). Граница цилиндра — **SDF + Bouzidi (no-slip)**;
вход/выход — **Zou-He** (скорость/давление); верх/низ канала — **free-slip (specular)**.
Только GPU/CuPy — **CPU-фолбэка нет**. Запуск — из этой папки: `python run.py`
(серия по блокировке: `python run_blockage.py`).
## Карта файлов (каждый компонент изолирован и легко меняется)
| Файл | Ответственность |
|---|---|
| `backend.py` | жёсткое требование CuPy/GPU, `DTYPE`, device-скаляры, `to_cpu` |
| `config.py` | все параметры модели + производные (τ, β, R, DL, cy/патч из Ny); `from_env()` |
| `lattice.py` | D2Q9 (c_i, w_i, OPP) + KBC-проектор `Ps` на сдвиг-моменты |
| `equilibrium.py` | `feq` (product-form, без `pow`), `macros` (ρ, u) |
| `collision.py` | **столкновение**: `kbc_collide` (энтропийный KBC-N1) — единственный оператор |
| `streaming.py` | **перенос**: `stream` (пул-схема через roll) |
| `geometry.py` | маски тела/стенок и SDF для L0 и L1 |
| `boundary.py` | `build_bc`/`apply_bc` (SDF+Bouzidi) + `channel_bc` (Zou-He) + `free_slip_walls` |
| `forces.py` | **силы**: галилей-инвариантный обмен импульсом `force_gmem` (Fx, Fy, **торк Tz**) + `coefficients` (Cd, Cl, **Cm**) |
| `forces_stress.py` | **кросс-чек силы**: ∮σ·n ds по контуру R+δ вокруг тела (независимый эстиматор) |
| `amr.py` | связка L0↔L1: `patch`/`ghost`/`fill`/`restrict` |
| `solver.py` | оркестратор: persistent-буферы, `step()`, `run()` (+ CUDA-граф) |
| `diagnostics.py` | `analyze`, St (параболическая интерполяция пика), кросс-чек, фиты серии по β |
| `visualization.py` | гифка |ω|, png-панель рядов (`series_2x_sdf.png`), прогресс-бар |
| `run.py` | точка входа (один прогон) |
| `run_blockage.py` | серия по блокировке Ny↑ → β↓, экстраполяция β→0, сводный npz+png |
| `run_factors.py` | факторное исследование постановки (вход/выход/uy-выход); реестр `out/factors.csv` |
## Где какая математика (для ревизии)
- **Столкновение** — `collision.py`. KBC-N1: `f ← f − β(2Δs + γΔh)`, где `Δs = Ps·(f−feq)` —
проекция на сдвиг-моменты, `γ` — энтропийный лимитер. Оператор жёстко зафиксирован:
модуль объявляет `collide = kbc_collide`, поля `cfg.collision` и реестра операторов
не существует (см. раздел «Статус» ниже). Чтобы поэкспериментировать с BGK или TRT,
придётся вводить и то и другое — это осознанно не сделано.
- **Перенос** — `streaming.py`. Чистая пул-схема; на физику влияет только корректность сдвигов.
- **Силы** — `forces.py`. Обмен импульсом по линкам тела:
`F = Σ_links c_i (f_i^{после столкн.} + f_ī^{после стриминга})`. Второй член — из поля ПОСЛЕ
стриминга (вернувшаяся Bouzidi-популяция); иначе ведущий симметричный член сокращается и Cd врёт.
Сила/момент меряются на **каждом** подшаге L1 и усредняются (анти-алиасинг; средний Cd не меняется).
**Торк** `Tz` — вокруг центра тела, точка приложения — пересечение линка со стенкой `r_f + q·c_i`
(Bouzidi-доля q уже лежит в BC); `Cm = 2Tz/(U²·DL²)`, для цилиндра ⟨Cm⟩≈0 — контроль симметрии.
- **Кросс-чек силы** — `forces_stress.py`. Независимый эстиматор — баланс импульса
контрольного объёма: `F = ∮[σ·n − ρu(u·n)] ds` по окружности R+δ (δ=2 тонкие ячейки);
`σ = −p′·I − (1−1/(2τ₁))·Π^neq_dev` (строго девиаторная проекция: след Π^neq в KBC
релаксирует с γβ, а не 1/τ). Конвективный член −ρu(u·n) обязателен при δ>0 — без него
систематика +4% по ⟨Cd⟩. Считается раз в `stress_every` шагов **вне CUDA-графа**; сравнение
по средним (⟨Cd⟩, rms Cl) — мгновенные ряды сдвинуты по фазе кольцом R..R+δ.
Согласие двух методов = считывание силы корректно.
- **Серия по блокировке** — `run_blockage.py`. D фиксирован, Ny ∈ {90,128,180} → β ∈ {0.178,
0.125, 0.089}; `cy` и y-границы патча выводятся из Ny автоматически (`config.__post_init__`).
Экстраполяция к β→0 двумя фитами (a+b·β и a+b·β²; спред интерсептов — неопределённость) —
прямое сравнение с безграничной литературой без ручной поправки (1−β)².
## ⚠ Сверка с первоисточниками (2026-08-14): найден дефект, все числа ниже требуют перепрогона
Сверка реализации со статьями в `docs/origins` (Karlin/Bösch/Chikatamarla, PRE **90**, 031302(R)
2014 — принцип; Bösch/Chikatamarla/Karlin, PRE **92**, 043309 2015 — алгоритм; arXiv:1507.02509 —
2D-реализация). **Ядро KBC-N1 подтверждено точным**: проектор `Ps` совпадает с аналитической
s-частью из ур.(10) 2D-статьи (ранг 2, идемпотентен, несёт ровно {N, Π_xy}); равновесие —
точный энтропийный максимизатор (сходится с численной максимизацией S при фикс. ρ, ρu);
γ по замкнутой формуле ур.(17)/(39) совпадает с корнем точного ур.(15); сдвиг-моменты
релаксируют ровно с 2β при любом γ; вязкость по затуханию сдвиговой волны воспроизводится
с ошибкой <0.1%.
**Дефект — порог `GEPS` в `kbc_collide`.** Он был АБСОЛЮТНЫМ (1e-6 в float32, режим по умолчанию),
тогда как `den = ⟨Δh|Δh⟩` квадратична по неравновесию и при разрешённой физике равна ~1e-7…1e-9.
Замерено на боевой конфигурации: откат `γ←2` срабатывал на **77–99% узлов**, то есть решатель
считал **чистый LBGK**, а не KBC. На эталоне 2D-статьи (двойной периодический сдвиговый слой,
Re=30000, N=128, разд. VII) это давало **+9.3%** по энстрофии на t=t_c (0.6599 против эталонных
0.6035); после исправления fp32 даёт 0.6038. Порог сделан относительным (`B.GREL`, доля от ‖Δ‖²);
на тех же полях он теперь не срабатывает нигде (min den/‖Δ‖² ≈ 1.5e-3 против порога 1e-8).
**Следствие: все количественные результаты ниже (Cd, rms Cl, St, серия по блокировке,
факторное исследование №0/№1) получены на дефектной ветке и подлежат перепрогону.**
Качественные выводы про ГУ (мода акустического резонатора, роль конвективного члена
в кросс-чеке) дефектом не затрагиваются.
Исправлено там же:
- **углы домена**: `stream()` на `roll` периодичен по обеим осям, а Zou-He стоял на срезе `1:-1` —
в 4 угловых узлах вход был напрямую связан с выходом (`f[1,0,0] == post[1,0,-1]`, величина ~0.12).
Порядок изменён на `free_slip_walls` → `channel_bc` по всему столбцу; заворот закрыт полностью.
- **диагностика ⟨ρ⟩** считалась по всему домену, включая фиктивные узлы внутри цилиндра
(смещение −5.4e-4 — ровно тот разряд, в котором ⟨ρ⟩ и цитируется). Теперь только по жидкости.
- **`refine` был зашит как 2** в `tau1`, `R01` и радиусе тела на L1/контуре σ·n. Обобщено
(при r=2 значения бит-в-бит прежние; при r=3 вязкость уровней теперь согласована).
**Отдельно — не дефект, а следствие выбора модели.** В KBC-N1 след тензора напряжений T отнесён
к h-части, поэтому объёмная вязкость плавает: ξ = c_s²(1/(γβ) − ½) (ур.36 статьи 2015, ур.57
2D-статьи). Замер на боевой конфигурации: γ ∈ [−4.1, 4.6], медиана ξ/ν ≈ 17, и на **2.1% узлов
ξ < 0** — акустическое АНТИдемпфирование. Это прямо релевантно диагнозу «недодемпфированный
акустический резонатор» ниже: KBC-N2 (T в s-части) даёт ξ = ν фиксированно и является
каноническим средством именно от этой проблемы. Модель не менялась — решение за автором.
## Статус: считывание силы валидировано; идёт факторное исследование постановки
Оператор столкновения зафиксирован — **KBC-N1 с энтропийным стабилизатором** (канонический
Karlin/Bösch/Chikatamarla 2014), без замен (никаких BGK/TRT).
**Полная документация исследования — в ноутбуке `../kbc_lbm.ipynb` (части VI, разделы 22–24).**
**Считывание силы доказано корректным** (прогон 100k, 174×90, 2026-06-09):
- кросс-чек GMEM vs ∮σ·n: dCd=4.1% (систематика объяснена конвективным членом −ρu(u·n),
добавлен в `forces_stress.py` — ожидаемое расхождение ≲1%);
- ⟨Cm⟩=−0.00003 (≈0 — симметрия), ⟨ρ⟩=1.001 стабильна, Cd=1.799 воспроизводит прежнюю
итерацию бит-в-бит (осреднение по подшагам не сместило среднее).
**Эксперимент №0 (серия по блокировке) — гипотеза бокового стеснения ОПРОВЕРГНУТА:**
Cd(β) немонотонен: 1.799 / 1.771 / **1.870** при Ny=90/128/180; rms Cl на широком канале
вырос (0.835), НЧ-модуляция (окна Cd 1.68–1.98). При этом **St(β→0)=0.181 ≈ лит. 0.183** —
кинематика верна, завышены давленческие амплитуды. Диагноз: продольные границы
(вход 2.5D с жёстким профилем; выход 8.3D Zou-He с жёстким uy=0 — отражает вихри).
**Эксперимент №1 (прогнан 2026-06-10) — факторное исследование** `run_factors.py`, 6 прогонов
при Ny=90 фикс. Итоги (полный разбор — ноутбук, раздел 24; реестр — `out/factors.csv`):
- конвективный член подтверждён: кросс-чек B0 4.1%→**1.0%**, F4 — **0.6%**;
- **F4 (мягкий uy-выход) — лучший по всем метрикам**: Cd 1.799→1.754, rms Cl 0.705→**0.557**,
St·(1−β)=**0.183** (точно лит.), ⟨ρ⟩=1.0000, нулевая НЧ-модуляция;
- **длинные домены без демпфера НЕвалидны** (F1/F2/F3 → смещённый режим ⟨ρ⟩≈1.66; F5 → NaN):
пара «вход-скорость / выход-давление» — недодемпфированный акустический резонатор
(затухание моды ∝ν(π/Nx)²; период осцилляций ⟨ρ⟩ у F5 ≈ 2Nx/c_s — прямая улика).
**Эксперимент №2 (в работе) — губка перед выходом**: плавный рост ν (×30, smoothstep) в
последних `sponge_len` столбцах L0; β(x) — поле (graph-safe), патч L1 губки не видит.
Факторы: F6_sponge (база+uy-extrap+губка 32 — валидация против F4), F7_long_sponge
(390×90, вход 6D/выход 18.3D — целевая постановка), F8_long_sp_uy0 (изоляция вклада губки).
Критерии: ⟨ρ⟩≈1.000 у всех; F7: Cd≈1.5–1.6, rms Cl<0.45. Затем эксперимент №3 — серия по β
на конфигурации F7.
| прогон | команда | артефакты в `out/` |
|---|---|---|
| смоук | `AMR_STEPS=2000 python run.py` | graph self-check OK, Cm≈0, кросс-чек ≲1% (с конв. членом) |
| основной | `python run.py` | `cyl_2x_sdf.gif`, `series_2x_sdf.png`, `probe_2x_sdf.npz` |
| серия по β | `python run_blockage.py` | `probe_blockage_Ny*.npz`, `blockage_summary.npz`, `blockage_extrapolation.png` |
| **факторы №2** | `AMR_FACTORS=F6_sponge,F7_long_sponge,F8_long_sp_uy0 python run_factors.py` | `factor_*.npz`, **`factors.csv`** (реестр; при смене схемы старый → `factors_vN.csv`), `factors_summary.png` |
Env-ручки: `AMR_NX`, `AMR_CX` (продольная геометрия; x-границы патча L1 выводятся из cx),
`AMR_OUTLET_UY=zero|extrapolate`, `AMR_SPONGE_LEN`/`AMR_SPONGE_NU` (губка),
`AMR_FACTORS=...` (подмножество факторов).
**Что привело сюда (хронология фиксов):**
1. Зонд скорости → L1; оценка St с параболической интерполяцией пика.
2. Сила → галилей-инвариантный `force_gmem` (Wen 2014), готов к подвижным телам.
3. Стартовое возмущение (поперечный sin-импульс входа) — запуск дорожки за единицы циклов.
4. Диагностика по окнам (`convergence_report`, трекинг ⟨ρ⟩) — вскрыла дрейф массы.
5. **Ключевой фикс:** грубое ГУ выхода (zero-gradient) копило массу → поток глох (Cd/rms→0 за 100k).
Заменено на **Zou-He скоростной вход + давление-выход** (`boundary.channel_bc`) — масса заякорена.
6. **Free-slip стенки** (`boundary.free_slip_walls`, specular) + умеренно расширенный домен (меньше
блокировка). Цилиндр остаётся no-slip.
7. Cm + осреднение по подшагам + кросс-чек ∮σ·n ds + серия по блокировке (текущая итерация).
**Прямой raw-матч на одной сетке непрактичен** (нужен `β<0.03 → Ny≈500+`); серия из трёх умеренных
Ny с экстраполяцией даёт безпоправочное сравнение за ~3 стоимости одного прогона (патч L1 —
основная работа — от Ny не зависит).
## CUDA Graphs
По умолчанию ВКЛ (`cfg.use_cuda_graph`, env `AMR_CUDAGRAPH=0` — выкл). **Захватываются успешно** (после
замены `cupy.stack`→`backend.pack`, отказа от cuBLAS в проекторе KBC и clip-литералов в ядре —
всё это убрало host→device при capture). На шаге t=1 — рантайм-самопроверка (граф vs eager); при
расхождении/сбое — откат на eager с печатью трейсбэка.