Files
NotBigGhostandClaude Opus 5 11ff7b79b4 Начальный коммит: 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>
2026-08-14 16:37:38 +03:00

266 lines
14 KiB
Python
Raw Permalink 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.
# Демо-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)")