Начальный коммит: Vulkan-редактор SimVulcan + исследование KBC-LBM

Состояние на момент заведения репозитория.

C++ приложение (src/, shaders/, tests/) — минимальный редактор 3D-моделей
на Vulkan 1.3: орбитальная камера, три опорные сетки через начало координат,
загрузка .obj с режимами отображения. Весь Vulkan изолирован в src/vk/.

Исследование (docs/) — оригинальные статьи по KBC (docs/origins) и
Python-решатель D2Q9 KBC-N1 с AMR 2x и SDF+Bouzidi (docs/theory).

В решателе перед коммитом исправлены дефекты, найденные сверкой с
первоисточниками: относительный порог знаменателя энтропийного стабилизатора
(абсолютный вырождал KBC в LBGK на 77-99% узлов), заворот вход/выход в углах
домена, диагностика средней плотности по фиктивным узлам тела, зашитый
refine=2. Подробности — docs/theory/solver_2x_sdf/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 16:37:38 +03:00
co-authored by Claude Opus 5
commit 11ff7b79b4
139 changed files with 88747 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
# ОСНОВНОЙ GPU-решатель (с SDF + Bouzidi): цилиндр, три версии (без AMR / 2× / 2×+4×).
# Граница тела — по знаковому полю расстояний + интерполированное отражение (Bouzidi);
# стенки канала — тот же механизм с q=0.5 (обычный halfway). Математика идентична _amr_anims_sdf.py.
# backend — CuPy (GPU) с откатом на numpy. В конце — сводное сравнение с версией без SDF.
# Запуск: python amr_cylinder_sdf_gpu.py (после amr_cylinder_gpu.py — для сводного сравнения)
import os, time, numpy as np
import amr_gpu_core as core
HERE = os.path.dirname(os.path.abspath(__file__))
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
print(f"[ОСНОВНОЙ · SDF+Bouzidi] backend={core.BACKEND}; домен {core.Nx}x{core.Ny}; steps={core.STEPS}; "
f"L1(2×) {core.P1['Nfx']}x{core.P1['Nfy']}; L2(4×) {core.P2['Nfx']}x{core.P2['Nfy']}")
def make_progress(label, width=32, every=0.1):
"""Колбэк-индикатор: рисует ASCII-шкалу по выполненным шагам (t из STEPS), обновляется на месте."""
st = {"t0": time.perf_counter(), "last": 0.0}
def cb(t, total):
now = time.perf_counter(); done = (t >= total)
if not done and (now - st["last"] < every): # троттлинг, чтобы не спамить консоль
return
st["last"] = now; frac = (t + 1) / (total + 1)
filled = int(round(width * frac)); bar = "#" * filled + "-" * (width - filled)
el = now - st["t0"]; eta = (el / frac - el) if frac > 0 else 0.0
print(f"\r {label:7}[{bar}] {frac*100:5.1f}% ({t}/{total}) {el:5.1f}s ETA {eta:5.1f}s",
end=("\n" if done else ""), flush=True)
return cb
t0 = time.perf_counter()
R0 = core.run("none", True, make_progress("none")); print(" none :", round(time.perf_counter()-t0, 1), "s")
R1 = core.run("amr2x", True, make_progress("amr2x")); print(" amr2x :", round(time.perf_counter()-t0, 1), "s")
R2 = core.run("nested", True, make_progress("nested")); print(" nested :", round(time.perf_counter()-t0, 1), "s")
core.save_gif(R1, "amr2x_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
core.save_gif(R2, "amr_nested_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
AS = [core.analyze(R0), core.analyze(R1), core.analyze(R2)]
core.print_table(AS, "С SDF — сравнение на реально собранных данных (установившийся режим)")
np.savez(os.path.join(HERE, "_amr_probe_data_sdf.npz"),
St=np.array([a["St"] for a in AS]), Cd=np.array([a["Cd"] for a in AS]),
Clrms=np.array([a["Clrms"] for a in AS]), uyrms=np.array([a["uyrms"] for a in AS]),
Cl0=AS[0]["Cl_s"], Cl1=AS[1]["Cl_s"], Cl2=AS[2]["Cl_s"],
uy0=AS[0]["uy"], uy1=AS[1]["uy"], uy2=AS[2]["uy"])
core.probe_figure(AS, R2, os.path.join(FIGS, "11f_amr_sdf_probe_comparison.png"),
"AMR на цилиндре (SDF+Bouzidi): сравнение трёх версий на данных зондов")
# ---- сводное сравнение: SDF против без-SDF (если есть свежие данные без SDF) ----
nosdf = os.path.join(HERE, "_amr_probe_data.npz")
if os.path.exists(nosdf) and ("St" in np.load(nosdf).files):
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
N = np.load(nosdf); Stn, Cdn = N["St"], N["Cd"]
Sts = np.array([a["St"] for a in AS]); Cds = np.array([a["Cd"] for a in AS])
print("\n=== СВОДНО: SDF vs без SDF (Cd, St) ===")
print(f"{'версия':14}{'St(noSDF)':>11}{'St(SDF)':>9}{'Cd(noSDF)':>11}{'Cd(SDF)':>9}")
for k, lab in enumerate(["без AMR", "2×", "2×+4×"]):
print(f"{lab:14}{Stn[k]:>11.3f}{Sts[k]:>9.3f}{Cdn[k]:>11.3f}{Cds[k]:>9.3f}")
print(f"{'лит. Re≈150':14}{0.183:>11.3f}{0.183:>9.3f}{1.330:>11.3f}{1.330:>9.3f}")
x = np.arange(3); w = 0.38
fig, axs = plt.subplots(1, 2, figsize=(13.5, 4.6))
axs[0].bar(x-w/2, Cdn, w, color="#9e9e9e", label="без SDF (ступенч.)")
axs[0].bar(x+w/2, Cds, w, color="#1f6feb", label="с SDF (Bouzidi)")
axs[0].axhline(1.33, color="k", ls="--", alpha=0.6, label="лит. Cd≈1.33")
axs[0].set_xticks(x); axs[0].set_xticklabels(["без AMR", "2×", "2×+4×"]); axs[0].set_ylabel("<Cd>")
axs[0].set_title("Сопротивление: SDF убирает завышение от ступенчатой границы")
axs[0].legend(fontsize=8); axs[0].grid(alpha=0.3, axis="y")
axs[1].bar(x-w/2, Stn, w, color="#9e9e9e", label="без SDF"); axs[1].bar(x+w/2, Sts, w, color="#1f6feb", label="с SDF")
axs[1].axhline(0.183, color="k", ls="--", alpha=0.6, label="лит. St≈0.18")
axs[1].set_xticks(x); axs[1].set_xticklabels(["без AMR", "2×", "2×+4×"]); axs[1].set_ylabel("St")
axs[1].set_title("Число Струхаля"); axs[1].legend(fontsize=8); axs[1].grid(alpha=0.3, axis="y")
fig.suptitle("Цилиндр: SDF (Bouzidi) против ступенчатого bounce-back — на реальных данных", fontweight="bold")
fig.savefig(os.path.join(FIGS, "11g_sdf_vs_nosdf.png"), dpi=110, bbox_inches="tight"); plt.close(fig)
print("saved 11g_sdf_vs_nosdf.png")
else:
print("\n(нет свежего _amr_probe_data.npz — сначала запусти amr_cylinder_gpu.py)")
print("DONE (SDF)")