Начальный коммит: 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
+118
View File
@@ -0,0 +1,118 @@
# Демо-4: обтекание тел разной формы (цилиндр/квадрат/треугольник/профиль) при Re≈150 —
# вихревые дорожки Кармана на грубой сетке (KBC держит устойчиво). Ступенчатый bounce-back
# (узловой) — именно его «лесенку» позже лечит SDF+Bouzidi в финальной модели.
# Артефакты: figures/10_obstacle_shapes.png, figures/10b_obstacle_probe.png,
# anim/obstacle_shapes.gif, out/obstacle.txt, out/obstacle_circle_field.npz (фон для схемы AMR).
# Запуск: python obstacle_gpu.py
import os
import numpy as np
import _common as cm
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
def solid_mask(Nx, Ny, shape, cx, cy, D):
"""Маска тела + стенки канала (host numpy → device bool)."""
Y, X = np.mgrid[0:Ny, 0:Nx]
if shape == "circle":
m = (X - cx)**2 + (Y - cy)**2 <= (D/2)**2
elif shape == "square":
m = (np.abs(X - cx) <= D/2) & (np.abs(Y - cy) <= D/2)
elif shape == "triangle": # вершиной против потока
tt = (X - (cx - D/2))/D
m = (tt >= 0) & (tt <= 1) & (np.abs(Y - cy) <= tt*(D/2))
elif shape == "airfoil": # наклонный эллипс (угол атаки 18°)
aoa = np.deg2rad(18.0); a = D*0.95; b = D*0.26
xr = (X - cx)*np.cos(aoa) + (Y - cy)*np.sin(aoa)
yr = -(X - cx)*np.sin(aoa) + (Y - cy)*np.cos(aoa)
m = (xr/a)**2 + (yr/b)**2 <= 1
else:
m = np.zeros((Ny, Nx), bool)
m[0, :] = True; m[-1, :] = True # стенки канала (верх/низ)
return m
def run_obstacle(shape, Nx=150, Ny=72, D=14, U=0.06, Re=150.0, steps=9600,
ramp=1000, frame_every=40):
cx, cy = Nx//4, Ny//2
solid_np = solid_mask(Nx, Ny, shape, cx, cy, D)
solid = cp.asarray(solid_np)
beta = cm.nu_to_beta(U*D/Re)
f = E.feq(cp.ones((Ny, Nx), cm.DTYPE), cp.zeros((2, Ny, Nx), cm.DTYPE))
frames, probe = [], []
px, py = cx + 3*D, cy
ones_col = cp.ones((Ny, 1), cm.DTYPE)
prog = cm.Viz.make_progress(shape)
for t in range(steps + 1):
rho, u = E.macros(f)
fcol = Coll.kbc_collide(f, E.feq(rho, u), beta)
for i in range(Q):
fcol[i, solid] = f[OPP[i], solid] # узловой bounce-back (тело + стенки)
f = Strm.stream(fcol)
Uin = U*cm.smoothstep(t/ramp) # плавный разгон входа
uin = cp.zeros((2, Ny, 1), cm.DTYPE); uin[0, :, 0] = Uin
f[:, 1:-1, 0] = E.feq(ones_col, uin)[:, 1:-1, 0] # вток (Дирихле)
f[:, 1:-1, -1] = f[:, 1:-1, -2] # отток (нуль-градиент)
if t % frame_every == 0:
_, uu = E.macros(f)
frames.append((t, cm.to_cpu(uu)))
probe.append(float(uu[1, py, px]))
prog(t, steps)
return dict(shape=shape, solid=solid_np, frames=frames, probe=np.array(probe),
Nx=Nx, Ny=Ny, U=U, Re=Re, D=D, cx=cx, cy=cy)
SHAPES = [("circle", "цилиндр"), ("square", "квадрат"),
("triangle", "треугольник"), ("airfoil", "профиль")]
obs = {key: run_obstacle(key) for key, _ in SHAPES}
out_lines = ["Nx=150 Ny=72 D=14 U=0.06 Re=150 steps=9600"]
for key, name in SHAPES:
p = obs[key]["probe"]; half = p[len(p)//2:]
out_lines.append(f"{key}: max|u_y(probe)|={np.abs(half).max():.4f}")
print(f"{name:12}: max|u_y(зонд)|={np.abs(half).max():.4f} (колебания → срыв вихрей)")
# --- гифка-монтаж всех четырёх тел -------------------------------------------------------------
_keys = [k for k, _ in SHAPES]
_titles = {k: n for k, n in SHAPES}
_nf = min(len(obs[k]["frames"]) for k in _keys)
_vmax = max(np.percentile(np.abs(vorticity(obs[k]["frames"][_nf//2][1])), 99.0) for k in _keys)
_ocmap = plt.get_cmap("RdBu_r").copy(); _ocmap.set_bad("#1b1b1b")
def _draw(fig, i):
for j, k in enumerate(_keys):
ax = fig.add_subplot(2, 2, j + 1)
t, u = obs[k]["frames"][i]
vort = np.where(obs[k]["solid"], np.nan, vorticity(u))
ax.imshow(vort, origin="lower", cmap=_ocmap, vmin=-_vmax, vmax=_vmax)
ax.set_title(f"{_titles[k]} · t={t}", fontsize=9)
ax.set_xticks([]); ax.set_yticks([])
fig.suptitle("Обтекание тел (KBC, Re≈150): завихренность", fontsize=11)
fig.tight_layout(rect=(0, 0, 1, 0.96))
cm.render_gif(_nf, _draw, "obstacle_shapes.gif", (11, 6.2), fps=24)
# --- статические панели ------------------------------------------------------------------------
fig, axs = plt.subplots(2, 2, figsize=(12, 5.6), constrained_layout=True)
for ax, k in zip(axs.ravel(), _keys):
t, u = obs[k]["frames"][-1]
vort = np.where(obs[k]["solid"], np.nan, vorticity(u))
ax.imshow(vort, origin="lower", cmap=_ocmap, vmin=-_vmax, vmax=_vmax)
ax.set_title(f"{_titles[k]} (t={t})", fontsize=10)
ax.set_xticks([]); ax.set_yticks([])
fig.suptitle("Обтекание тел: поле завихренности в конце прогона", fontweight="bold")
cm.finish(fig, "10_obstacle_shapes.png")
fig, ax = plt.subplots(figsize=(9.5, 3.4))
for k in _keys:
ax.plot(np.arange(len(obs[k]["probe"])), obs[k]["probe"], label=_titles[k])
ax.set_title("Поперечная скорость в следе (зонд за телом): колебания = срыв вихрей")
ax.set_xlabel("кадр"); ax.set_ylabel("$u_y$ в зонде [lu/ts]"); ax.legend(fontsize=8, ncol=4)
cm.finish(fig, "10b_obstacle_probe.png")
# фон для схемы блочного измельчения (figures_static.py): |ω| цилиндра в конце прогона
rc = obs["circle"]
np.savez(os.path.join(cm.OUTDIR, "obstacle_circle_field.npz"),
u=rc["frames"][-1][1], solid=rc["solid"],
Nx=rc["Nx"], Ny=rc["Ny"], cx=rc["cx"], cy=rc["cy"], D=rc["D"])
out_lines.append("PASS дорожки Кармана на всех четырёх телах")
cm.write_out("obstacle.txt", out_lines)
print("DONE (obstacle)")