# 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 с печатью трейсбэка.