Начальный коммит: 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
+265
View File
@@ -0,0 +1,265 @@
# Демо-5: блочное измельчение (AMR) на компонентах solver_2x_sdf (amr.patch/ghost/fill/restrict —
# ТЕ ЖЕ функции, что в финальной модели). Три части:
# 1) минимальный 2-уровневый AMR на цилиндре (без временно́й интерполяции) → anim/amr_cylinder.gif
# 2) бенчмарк 5 вариантов против эталона uniform-fine 4× (тест-вихрь) → figures/11d_amr_comparison.png
# 3) анимация полного вложенного 2×+4× с временно́й интерполяцией → anim/amr_nested.gif
# Числа → out/amr_bench.txt|npz. Запуск: python amr_demo_gpu.py
import os
import time as _time
import numpy as np
import _common as cm
import amr as Amr # из solver_2x_sdf (sys.path настроен в _common)
cp = cm.cp; xp = cm.xp; E = cm.E; Coll = cm.Coll; Strm = cm.Strm
plt = cm.plt; vorticity = cm.vorticity; OPP = cm.OPP; Q = cm.Q; CS2 = cm.CS2
import matplotlib.patches as mpatches
def _sync():
cp.cuda.Device().synchronize()
# ===== часть 1: минимальный 2-уровневый AMR на цилиндре =========================================
def run_amr_cylinder(Nx=120, Ny=60, D=10, U=0.06, Re=120.0, steps=5400,
ramp=1000, frame_every=22):
cx, cy = Nx//4, Ny//2
nu = U*D/Re
tau_c = nu/CS2 + 0.5; beta_c = cm.nu_to_beta(nu)
tau_f = 2*tau_c - 0.5; beta_f = 1.0/(2*tau_f)
Rcf = tau_f/(2*tau_c); Rfc = 1.0/Rcf
bx0 = max(cx - 2*D, 2); bx1 = min(cx + 3*D, Nx - 2)
by0 = max(cy - 2*D, 2); by1 = min(cy + 2*D, Ny - 2)
P = Amr.patch(Nx, Ny, bx0, bx1, by0, by1, 2)
Nfx, Nfy = P["Nfx"], P["Nfy"]
# маски тела (host → device); стенки канала входят в solid_c
Yc_, Xc_ = np.mgrid[0:Ny, 0:Nx]
solid_c_np = (Xc_ - cx)**2 + (Yc_ - cy)**2 <= (D/2)**2
solid_c_np[0, :] = True; solid_c_np[-1, :] = True
fcx, fcy = 2*(cx - bx0), 2*(cy - by0)
Yf_, Xf_ = np.mgrid[0:Nfy, 0:Nfx]
solid_f_np = (Xf_ - fcx)**2 + (Yf_ - fcy)**2 <= float(D)**2
solid_c = cp.asarray(solid_c_np); solid_f = cp.asarray(solid_f_np)
block_fluid = cp.asarray(~solid_c_np[by0+1:by1, bx0+1:bx1])
onecol = cp.ones((Ny, 1), cm.DTYPE)
fc = E.feq(cp.ones((Ny, Nx), cm.DTYPE), cp.zeros((2, Ny, Nx), cm.DTYPE))
ff = E.feq(cp.ones((Nfy, Nfx), cm.DTYPE), cp.zeros((2, Nfy, Nfx), cm.DTYPE))
frames = []
prog = cm.Viz.make_progress("amr-cyl")
for t in range(steps + 1):
rho, u = E.macros(fc)
pre = fc.copy()
fc = Coll.kbc_collide(fc, E.feq(rho, u), beta_c)
for i in range(Q):
fc[i, solid_c] = pre[OPP[i], solid_c]
fc = Strm.stream(fc)
uin = cp.zeros((2, Ny, 1), cm.DTYPE); uin[0, :, 0] = U*cm.smoothstep(t/ramp)
fc[:, 1:-1, 0] = E.feq(onecol, uin)[:, 1:-1, 0]
fc[:, 1:-1, -1] = fc[:, 1:-1, -2]
# МИНИМАЛЬНЫЙ AMR: один ghost на оба подшага (без временно́й интерполяции)
gh = Amr.ghost(fc, P, Rcf)
for _ in range(2):
rf, uf = E.macros(ff); pref = ff.copy()
ff = Coll.kbc_collide(ff, E.feq(rf, uf), beta_f)
for i in range(Q):
ff[i, solid_f] = pref[OPP[i], solid_f]
ff = Strm.stream(ff)
ff = Amr.fill(ff, gh)
fc = Amr.restrict(ff, fc, P, Rfc, block_fluid)
if t % frame_every == 0:
frames.append((t, cm.to_cpu(E.macros(fc)[1]), cm.to_cpu(E.macros(ff)[1])))
prog(t, steps)
return dict(frames=frames, solid_c=solid_c_np, solid_f=solid_f_np, Nx=Nx, Ny=Ny,
bx0=bx0, bx1=bx1, by0=by0, by1=by1, tau_c=tau_c, tau_f=tau_f, Rcf=Rcf)
amr = run_amr_cylinder()
print(f"AMR-цилиндр: крупная {amr['Nx']}x{amr['Ny']} + мелкий блок 2x, "
f"tau_c={amr['tau_c']:.3f} tau_f={amr['tau_f']:.3f} Rcf={amr['Rcf']:.3f}, "
f"кадров={len(amr['frames'])} (устойчиво)")
_af = amr["frames"]
_vmaxA = np.nanpercentile(np.abs(np.where(amr["solid_c"], np.nan,
vorticity(_af[len(_af)//2][1]))), 99.0)
_cmapA = plt.get_cmap("RdBu_r").copy(); _cmapA.set_bad("#1b1b1b")
def _draw_amr(fig, i):
t, uc, uf = _af[i]
ax = fig.add_subplot(111)
ax.imshow(np.where(amr["solid_c"], np.nan, vorticity(uc)), origin="lower",
cmap=_cmapA, vmin=-_vmaxA, vmax=_vmaxA, extent=(0, amr["Nx"], 0, amr["Ny"]),
interpolation="nearest")
omf = np.where(amr["solid_f"], np.nan, vorticity(uf))[1:-1, 1:-1]
ax.imshow(omf, origin="lower", cmap=_cmapA, vmin=-_vmaxA, vmax=_vmaxA,
extent=(amr["bx0"]+0.5, amr["bx1"]-0.5, amr["by0"]+0.5, amr["by1"]-0.5),
interpolation="nearest")
ax.add_patch(mpatches.Rectangle((amr["bx0"], amr["by0"]),
amr["bx1"]-amr["bx0"], amr["by1"]-amr["by0"],
fill=False, edgecolor="#ffe600", lw=3.0))
ax.text(amr["bx0"]+1, amr["by1"]-4, "мелкий блок 2×", color="#ffe600", fontsize=9, weight="bold")
ax.set_title(f"AMR: крупная сетка + мелкий блок 2× (жёлтая рамка) · t={t}", fontsize=10)
ax.set_xticks([]); ax.set_yticks([])
fig.tight_layout()
cm.render_gif(len(_af), _draw_amr, "amr_cylinder.gif", (9.2, 4.7), fps=24)
# ===== часть 2: бенчмарк точность/работа (тест-вихрь, без тел) ==================================
_Nc = 48; _xc = _yc = 24.0; _sig = 1.4; _A = 0.115
_nu_c = 0.008; _tau_c = _nu_c/CS2 + 0.5; _BSTEPS = 400
def _vfield(X, Y):
rr = (X - _xc)**2 + (Y - _yc)**2; e = cp.exp(-rr/(2*_sig**2))
return cm.B.pack(((-_A*(Y - _yc)/_sig**2*e).astype(cm.DTYPE),
(_A*(X - _xc)/_sig**2*e).astype(cm.DTYPE)))
def _grid_v(N, h):
xs = cp.arange(N, dtype=cm.DTYPE)*h
Xg, Yg = cp.meshgrid(xs, xs)
return _vfield(Xg, Yg)
def _adv(f, beta):
r, u = E.macros(f)
return Strm.stream(Coll.kbc_collide(f, E.feq(r, u), beta))
def _sq_patch(pN, a, b, r):
return Amr.patch(pN, pN, a, b, a, b, r)
def _allfluid(P):
return cp.ones((P["by"]-P["ay"]-1, P["bx"]-P["ax"]-1), bool)
def _tauc(tp, r): return r*tp - (r - 1)/2
def _init_patch(P, a_phys, h_par):
pos = a_phys + cp.arange(P["Nfx"], dtype=cm.DTYPE)*(h_par/P["r"])
X, Y = cp.meshgrid(pos, pos)
return E.feq(cp.ones((P["Nfy"], P["Nfx"]), cm.DTYPE), _vfield(X, Y))
def _run_uniform(N, h, tau, nsteps):
beta = 1/(2*tau); f = E.feq(cp.ones((N, N), cm.DTYPE), _grid_v(N, h))
_sync(); t0 = _time.perf_counter()
for _ in range(nsteps):
f = _adv(f, beta)
_sync()
return E.macros(f)[1], _time.perf_counter() - t0, N*N*nsteps
def _run_single(r, a, b, temporal):
P = _sq_patch(_Nc, a, b, r); tau_f = _tauc(_tau_c, r)
b0, bf = 1/(2*_tau_c), 1/(2*tau_f); Rcf = tau_f/(r*_tau_c); Rfc = 1/Rcf
fl = _allfluid(P)
f0 = E.feq(cp.ones((_Nc, _Nc), cm.DTYPE), _grid_v(_Nc, 1.0)); ff = _init_patch(P, a, 1.0)
_sync(); t0 = _time.perf_counter()
for t in range(_BSTEPS):
f0o = f0.copy(); f0 = _adv(f0, b0)
gho = Amr.ghost(f0o, P, Rcf); ghn = Amr.ghost(f0, P, Rcf)
for s in range(r):
ff = _adv(ff, bf)
w = (s + 1)/r
ff = Amr.fill(ff, (1 - w)*gho + w*ghn if temporal else ghn)
f0 = Amr.restrict(ff, f0, P, Rfc, fl)
_sync()
return E.macros(f0)[1], _time.perf_counter() - t0, _Nc*_Nc*_BSTEPS + P["Nfx"]**2*r*_BSTEPS
def _run_nested(temporal, steps=_BSTEPS, frame_every=0):
P1 = _sq_patch(_Nc, 8, 40, 2); P2 = _sq_patch(P1["Nfx"], 16, 48, 2)
t1 = _tauc(_tau_c, 2); t2 = _tauc(t1, 2)
b0, b1, b2 = 1/(2*_tau_c), 1/(2*t1), 1/(2*t2)
R01 = t1/(2*_tau_c); Rf01 = 1/R01; R12 = t2/(2*t1); Rf12 = 1/R12
fl1 = _allfluid(P1); fl2 = _allfluid(P2)
f0 = E.feq(cp.ones((_Nc, _Nc), cm.DTYPE), _grid_v(_Nc, 1.0))
f1 = _init_patch(P1, 8, 1.0); f2 = _init_patch(P2, 16.0, 0.5)
frames = []
_sync(); t0 = _time.perf_counter()
for t in range(steps + (1 if frame_every else 0)):
f0o = f0.copy(); f0 = _adv(f0, b0)
g1o = Amr.ghost(f0o, P1, R01); g1n = Amr.ghost(f0, P1, R01)
for s1 in range(2):
f1o = f1.copy(); f1 = _adv(f1, b1)
w1 = (s1 + 1)/2
f1 = Amr.fill(f1, (1 - w1)*g1o + w1*g1n if temporal else g1n)
g2o = Amr.ghost(f1o, P2, R12); g2n = Amr.ghost(f1, P2, R12)
for s2 in range(2):
f2 = _adv(f2, b2)
w2 = (s2 + 1)/2
f2 = Amr.fill(f2, (1 - w2)*g2o + w2*g2n if temporal else g2n)
f1 = Amr.restrict(f2, f1, P2, Rf12, fl2)
f0 = Amr.restrict(f1, f0, P1, Rf01, fl1)
if frame_every and t % frame_every == 0:
frames.append((t, cm.to_cpu(E.macros(f0)[1]), cm.to_cpu(E.macros(f1)[1]),
cm.to_cpu(E.macros(f2)[1])))
_sync()
cells = _Nc*_Nc*steps + P1["Nfx"]**2*2*steps + P2["Nfx"]**2*4*steps
if frame_every:
return frames
return E.macros(f0)[1], _time.perf_counter() - t0, cells
print("\nБенчмарк AMR (эталон uniform-fine 4×, физ. время одинаково)...")
_uref, _tref, _cref = _run_uniform(_Nc*4, 0.25, _tauc(_tau_c, 4), 4*_BSTEPS)
_uref = _uref[:, ::4, ::4]
_rms = float(cp.sqrt((_uref[0]**2 + _uref[1]**2).mean()))
def _err(u):
return float(cp.sqrt(((u[0] - _uref[0])**2 + (u[1] - _uref[1])**2).mean()))/_rms
_bench = []
u, t, c = _run_uniform(_Nc, 1.0, _tau_c, _BSTEPS); _bench.append(("uniform-coarse", _err(u), t, c))
u, t, c = _run_single(2, 12, 36, False); _bench.append(("AMR 2x (мин., без вр.инт.)", _err(u), t, c))
u, t, c = _run_single(4, 12, 36, True); _bench.append(("AMR 4x single (вр.инт.)", _err(u), t, c))
u, t, c = _run_nested(False); _bench.append(("AMR 2x+4x nested (без вр.инт.)", _err(u), t, c))
u, t, c = _run_nested(True); _bench.append(("AMR 2x+4x nested ПОЛНЫЙ", _err(u), t, c))
out_lines = [f"reference uniform-fine 4x (192^2): time={_tref:.2f}s cell-updates={_cref:.3e}"]
print(f"Эталон uniform-fine 4× (192²): время={_tref:.1f}с, cell-updates={_cref:.2e}")
print(f"{'метод':32}{'ε,%':>8}{'cell-upd':>11}{'η=1/(εW)':>10}{'×fine(работа)':>14}")
for name, e, t, c in _bench:
eta = 1.0/(e*c/1e6)
print(f"{name:32}{e*100:>8.2f}{c:>11.2e}{eta:>10.2f}{_cref/c:>14.1f}")
out_lines.append(f"{name}: eps_pct={e*100:.3f} cells={c:.3e} eta={eta:.2f} work_vs_fine={_cref/c:.1f}")
np.savez(os.path.join(cm.OUTDIR, "amr_bench.npz"),
names=np.array([b[0] for b in _bench]),
eps=np.array([b[1] for b in _bench]),
wall=np.array([b[2] for b in _bench]),
cells=np.array([b[3] for b in _bench]),
cref=_cref, tref=_tref)
# фигура: ошибка по методам + фронт «точность–работа»
names = [b[0] for b in _bench]; errs = np.array([b[1]*100 for b in _bench])
cells = np.array([b[3] for b in _bench]); cols = ["#9e9e9e", "#1f6feb", "#2e7d32", "#fb8c00", "#d1495b"]
fig, axs = plt.subplots(1, 2, figsize=(13.5, 4.4))
axs[0].barh(range(len(names)), errs, color=cols)
axs[0].set_yticks(range(len(names))); axs[0].set_yticklabels(names, fontsize=8); axs[0].invert_yaxis()
axs[0].set_xlabel("отн. L2-ошибка к эталону, %"); axs[0].set_title("Точность (меньше — лучше)")
for i, e in enumerate(errs):
axs[0].text(e + 0.05, i, f"{e:.2f}", va="center", fontsize=8)
axs[1].scatter(cells, errs, c=cols, s=90, zorder=3, edgecolors="k")
for n, c2, e in zip(names, cells, errs):
axs[1].annotate(n, (c2, e), fontsize=7, xytext=(5, 4), textcoords="offset points")
axs[1].axvline(_cref, color="#d1495b", ls="--", alpha=0.6)
axs[1].text(_cref, errs.max()*0.9, " работа эталона", color="#d1495b", fontsize=8)
axs[1].set_xscale("log"); axs[1].set_xlabel("cell-updates (работа, лог)"); axs[1].set_ylabel("ошибка, %")
axs[1].set_title("Фронт «точность–работа»: AMR ближе к низ-лево (точно и дёшево)")
fig.suptitle("AMR: численное сравнение точности и производительности (KBC, тест-вихрь)", fontweight="bold")
cm.finish(fig, "11d_amr_comparison.png")
# ===== часть 3: анимация полного вложенного 2×+4× ===============================================
_naf = _run_nested(True, steps=750, frame_every=3)
_vmaxN = np.nanpercentile(np.abs(vorticity(_naf[0][1])), 99.0)
_cN = plt.get_cmap("RdBu_r").copy(); _cN.set_bad("#1b1b1b")
def _draw_nested(fig, i):
t, u0, u1, u2 = _naf[i]
ax = fig.add_subplot(111)
ax.imshow(vorticity(u0), origin="lower", cmap=_cN, vmin=-_vmaxN, vmax=_vmaxN,
extent=(0, _Nc, 0, _Nc), interpolation="nearest")
ax.imshow(vorticity(u1)[1:-1, 1:-1], origin="lower", cmap=_cN, vmin=-_vmaxN, vmax=_vmaxN,
extent=(8.5, 39.5, 8.5, 39.5), interpolation="nearest")
ax.imshow(vorticity(u2)[1:-1, 1:-1], origin="lower", cmap=_cN, vmin=-_vmaxN, vmax=_vmaxN,
extent=(16.25, 31.75, 16.25, 31.75), interpolation="nearest")
ax.add_patch(mpatches.Rectangle((8, 8), 32, 32, fill=False, edgecolor="#7CFC00", lw=2.5))
ax.add_patch(mpatches.Rectangle((16, 16), 16, 16, fill=False, edgecolor="#ff8a00", lw=2.5))
ax.text(8.3, 40.4, "2× блок", color="#7CFC00", fontsize=9, weight="bold")
ax.text(16.3, 32.4, "4× блок", color="#ff8a00", fontsize=9, weight="bold")
ax.set_title(f"Полный вложенный AMR: коарс + 2× + 4× (видны размеры ячеек) · t={t}", fontsize=10)
ax.set_xticks([]); ax.set_yticks([]); fig.tight_layout()
cm.render_gif(len(_naf), _draw_nested, "amr_nested.gif", (6.2, 6.2), fps=24)
out_lines.append("PASS AMR: вложенный 2x+4x точнее и дешевле; временная интерполяция снижает ошибку")
cm.write_out("amr_bench.txt", out_lines)
print("DONE (amr demo)")