Начальный коммит: 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:
@@ -0,0 +1,36 @@
|
||||
# demos_gpu — GPU-прогоны демо для ноутбука kbc_lbm.ipynb
|
||||
|
||||
Ноутбук **не исполняет код** — он встраивает готовые артефакты (png/gif) и цитирует числа.
|
||||
Все симуляции здесь, **только GPU/CuPy** (CPU-фолбэка нет). Физика не дублируется: решётка,
|
||||
равновесие, KBC-столкновение, перенос и AMR-связка импортируются из `../solver_2x_sdf` —
|
||||
демо тем самым гоняют **те же компоненты, что и финальная модель**.
|
||||
|
||||
## Запуск (на GPU-сервере, из этой папки)
|
||||
|
||||
```sh
|
||||
python checks_gpu.py # самопроверки математики → out/checks.txt
|
||||
python tgv_gpu.py # Тейлор–Грин: вязкость, H-теорема → figures/07*, anim/tgv_decay.gif
|
||||
python shear_gpu.py # сдвиговый слой BGK vs KBC → figures/08*, anim/shear_bgk_vs_kbc.gif
|
||||
python cavity_gpu.py # каверна Re=1000 vs Ghia → figures/09*, anim/cavity_transient.gif
|
||||
python obstacle_gpu.py # 4 тела, дорожки Кармана → figures/10*, anim/obstacle_shapes.gif
|
||||
python amr_demo_gpu.py # AMR: демо + бенчмарк + вложенный → figures/11d*, anim/amr_{cylinder,nested}.gif
|
||||
python figures_static.py # стенсили/блок-схема/схема AMR → figures/01,06,11c,11_d3q27 (после obstacle!)
|
||||
```
|
||||
|
||||
`figures_static.py` — единственный скрипт без GPU (чистый matplotlib, можно гонять локально);
|
||||
схему `11c` он перерисовывает по фону из `out/obstacle_circle_field.npz`, который пишет
|
||||
`obstacle_gpu.py` — поэтому его запускать **после** obstacle.
|
||||
|
||||
Env: `AMR_FP64=1` — двойная точность (жёстче пороги самопроверок в checks_gpu).
|
||||
|
||||
| Скрипт | Время (примерно, совр. GPU) |
|
||||
|---|---|
|
||||
| checks_gpu | секунды |
|
||||
| tgv_gpu | ~1 мин (10k шагов, 96²) |
|
||||
| shear_gpu | ~1–2 мин (2×8k, 128²) |
|
||||
| cavity_gpu | ~1–2 мин (до 14k, 110²) |
|
||||
| obstacle_gpu | ~3–5 мин (4×9.6k, 150×72) |
|
||||
| amr_demo_gpu | ~5–10 мин (демо + 6 бенчмарков + анимация) |
|
||||
|
||||
Артефакты идут в общие каталоги `../figures` и `../anim` под **теми же именами**, что
|
||||
исторически использовал ноутбук; числовые сводки — в `out/*.txt|npz`.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Общая инфраструктура GPU-демо (ноутбук kbc_lbm.ipynb код не исполняет — только встраивает
|
||||
# артефакты, которые генерят эти скрипты на GPU-сервере).
|
||||
#
|
||||
# Физика НЕ дублируется: решётка/равновесие/столкновение/перенос импортируются из
|
||||
# ../solver_2x_sdf (те же модули, что и в финальной модели) — демо тем самым доказывают,
|
||||
# что КОМПОНЕНТЫ финальной модели воспроизводят классические бенчмарки.
|
||||
# Здесь только то, чего в модели нет намеренно: полиномиальное равновесие и BGK
|
||||
# (для сравнений «BGK vs KBC»), H-функция, γ-поле для визуализации, гифки.
|
||||
import os
|
||||
import sys
|
||||
import io as _io
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
THEORY = os.path.dirname(HERE)
|
||||
sys.path.insert(0, os.path.join(THEORY, "solver_2x_sdf"))
|
||||
|
||||
import backend as B # GPU-only: жёсткая ошибка без CuPy (CPU-фолбэка нет намеренно)
|
||||
import lattice as L
|
||||
import equilibrium as E
|
||||
import collision as Coll
|
||||
import streaming as Strm
|
||||
import visualization as Viz # make_progress (ASCII-бар)
|
||||
|
||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE; to_cpu = B.to_cpu
|
||||
Q = L.Q; CS2 = L.CS2; OPP = L.OPP; Cx = L.Cx; Cy = L.Cy
|
||||
|
||||
import numpy as np
|
||||
|
||||
# пути артефактов: фигуры/гифки — общие каталоги theory, числа — demos_gpu/out
|
||||
FIGDIR = os.path.join(THEORY, "figures"); os.makedirs(FIGDIR, exist_ok=True)
|
||||
ANIMDIR = os.path.join(THEORY, "anim"); os.makedirs(ANIMDIR, exist_ok=True)
|
||||
OUTDIR = os.path.join(HERE, "out"); os.makedirs(OUTDIR, exist_ok=True)
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8") # кириллица/греческие в print при cp1251-консоли
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
plt.rcParams.update({
|
||||
"figure.dpi": 110, "savefig.dpi": 120, "font.size": 11,
|
||||
"axes.titlesize": 13, "axes.titleweight": "bold",
|
||||
"axes.grid": True, "grid.alpha": 0.25, "axes.axisbelow": True,
|
||||
"figure.facecolor": "white", "axes.facecolor": "#fbfbfd",
|
||||
})
|
||||
|
||||
# пороги самопроверок под точность бэкенда (демо обычно гоняются в float32)
|
||||
FP64 = (DTYPE == cp.float64)
|
||||
TOL_EXACT = 1e-12 if FP64 else 2e-5 # «точные» тождества (моменты равновесия и т.п.)
|
||||
|
||||
def finish(fig, name):
|
||||
"""Сохранить фигуру в figures/ под тем же именем, что использовал ноутбук."""
|
||||
fig.savefig(os.path.join(FIGDIR, name), bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
print("PNG:", name)
|
||||
|
||||
def write_out(name, lines):
|
||||
"""Дописать результаты в demos_gpu/out/<name> (ноутбук цитирует эти числа в markdown)."""
|
||||
with open(os.path.join(OUTDIR, name), "w", encoding="utf-8") as fh:
|
||||
fh.write("\n".join(lines) + "\n")
|
||||
print("OUT:", name)
|
||||
|
||||
# ----- дополнения к модели (нужны только демо) ------------------------------------------------
|
||||
|
||||
def feq_poly(rho, u):
|
||||
"""Полиномиальное равновесие O(u²) — стандарт BGK (в модели НЕ используется; здесь —
|
||||
для демонстрации «BGK+poly разрушается там, где KBC+entropic держит»)."""
|
||||
CXa = L.CXa[:, None, None]; CYa = L.CYa[:, None, None]; Wa = L.Wa[:, None, None]
|
||||
cu = CXa*u[0] + CYa*u[1]
|
||||
usq = u[0]**2 + u[1]**2
|
||||
return Wa * rho * (1.0 + cu/CS2 + cu**2/(2.0*CS2*CS2) - usq/(2.0*CS2))
|
||||
|
||||
def bgk_collide(f, fe, beta):
|
||||
"""Классический BGK: f − ω(f−feq), ω=2β. Эквивалент KBC при γ=2."""
|
||||
return f - 2.0*beta*(f - fe)
|
||||
|
||||
def kbc_gamma(f, fe, beta):
|
||||
"""Поле энтропийного стабилизатора γ (для визуализации; сам оператор — collision.kbc_collide,
|
||||
формула та же: γ = 1/β − (2−1/β)·⟨Δs|Δh⟩/⟨Δh|Δh⟩)."""
|
||||
dfn = f - fe
|
||||
ds = Coll._project_shift(dfn)
|
||||
dh = dfn - ds; inv = 1.0/fe
|
||||
num = (ds*dh*inv).sum(0); den = (dh*dh*inv).sum(0)
|
||||
nrm = (dfn*dfn*inv).sum(0) # тот же относительный порог, что в collision.py
|
||||
ok = den > B.GREL*nrm
|
||||
return xp.where(ok, 1.0/beta - (2.0 - 1.0/beta)*num/xp.where(ok, den, B.ONE), B.TWO)
|
||||
|
||||
def nu_to_beta(nu):
|
||||
return 1.0 / (2.0 * (nu / CS2 + 0.5))
|
||||
|
||||
def H_function(f):
|
||||
"""Дискретная H-функция H = Σ f_i ln(f_i/w_i) (поле)."""
|
||||
fp = xp.maximum(f, 1e-30)
|
||||
return (fp * xp.log(fp / L.Wa[:, None, None])).sum(0)
|
||||
|
||||
def smoothstep(x):
|
||||
x = min(max(x, 0.0), 1.0)
|
||||
return x*x*(3.0 - 2.0*x)
|
||||
|
||||
# ----- host-постобработка (numpy: кадры уже перенесены на хост) --------------------------------
|
||||
|
||||
def vorticity(u):
|
||||
"""ω = ∂x u_y − ∂y u_x центральными разностями (host, по кадру)."""
|
||||
duy_dx = (np.roll(u[1], -1, 1) - np.roll(u[1], 1, 1)) * 0.5
|
||||
dux_dy = (np.roll(u[0], -1, 0) - np.roll(u[0], 1, 0)) * 0.5
|
||||
return duy_dx - dux_dy
|
||||
|
||||
def render_gif(n_frames, draw_fn, name, figsize, fps=18, dpi=80):
|
||||
"""Отрисовать n_frames кадров (draw_fn(fig, i)) и сохранить зацикленный GIF в anim/."""
|
||||
from PIL import Image
|
||||
path = os.path.join(ANIMDIR, name)
|
||||
fig = plt.figure(figsize=figsize, dpi=dpi)
|
||||
pil = []
|
||||
for i in range(n_frames):
|
||||
fig.clf()
|
||||
draw_fn(fig, i)
|
||||
buf = _io.BytesIO()
|
||||
fig.savefig(buf, format="png")
|
||||
buf.seek(0)
|
||||
pil.append(Image.open(buf).convert("RGB").convert("P", palette=Image.ADAPTIVE))
|
||||
plt.close(fig)
|
||||
pil[0].save(path, save_all=True, append_images=pil[1:],
|
||||
duration=int(1000 / fps), loop=0, optimize=True)
|
||||
print("GIF:", name, f"({n_frames} кадров)")
|
||||
return path
|
||||
@@ -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)")
|
||||
@@ -0,0 +1,122 @@
|
||||
# Демо-3: каверна с движущейся крышкой (Re=1000) против эталона Ghia, Ghia & Shin (1982).
|
||||
# Стенки — bounce-back; крышка — bounce-back с поправкой импульса −2wᵢρ(cᵢ·u_w)/cs².
|
||||
# Сетка намеренно грубая, BB первого порядка → совпадение качественное.
|
||||
# Артефакты: figures/09_cavity_ghia.png, anim/cavity_transient.gif, out/cavity.txt|npz.
|
||||
# Запуск: python cavity_gpu.py
|
||||
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; OPP = cm.OPP; Q = cm.Q; CS2 = cm.CS2
|
||||
|
||||
UP = [2, 5, 6] # c_iy = +1
|
||||
DOWN = [4, 7, 8] # c_iy = -1
|
||||
RIGHT = [1, 5, 8] # c_ix = +1
|
||||
LEFT = [3, 6, 7] # c_ix = -1
|
||||
|
||||
def run_cavity(N=110, Re=1000.0, U=0.05, steps=14000, tol=1e-7, check=500, frame_every=58):
|
||||
nu = U*N/Re
|
||||
beta = cm.nu_to_beta(nu)
|
||||
rho = cp.ones((N, N), cm.DTYPE)
|
||||
u = cp.zeros((2, N, N), cm.DTYPE)
|
||||
f = E.feq(rho, u)
|
||||
W = cm.to_cpu(cm.L.Wa).astype(float)
|
||||
prev = None
|
||||
frames = []
|
||||
prog = cm.Viz.make_progress("cavity")
|
||||
t = 0
|
||||
for t in range(steps):
|
||||
rho, u = E.macros(f)
|
||||
if t % frame_every == 0:
|
||||
frames.append((t, cm.to_cpu(u)))
|
||||
fcol = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
||||
f = Strm.stream(fcol)
|
||||
# bounce-back: на каждой стенке достраиваем входящие в область направления
|
||||
for i in UP: # низ (y=0)
|
||||
f[i, 0, :] = fcol[OPP[i], 0, :]
|
||||
for i in RIGHT: # лево (x=0)
|
||||
f[i, :, 0] = fcol[OPP[i], :, 0]
|
||||
for i in LEFT: # право (x=N-1)
|
||||
f[i, :, -1] = fcol[OPP[i], :, -1]
|
||||
for i in DOWN: # верх (крышка) + поправка подвижной стенки
|
||||
io = OPP[i]
|
||||
corr = 2.0*W[io]*(cm.Cx[io]*U)/CS2 # u_w=(U,0): c_y-член равен нулю
|
||||
f[i, -1, :] = fcol[io, -1, :] - corr
|
||||
if t % check == 0 and t > 0:
|
||||
cur = u.copy()
|
||||
if prev is not None and float(xp.abs(cur - prev).max()) < tol:
|
||||
print(f"\ncavity: сошлось по полю скорости на шаге {t}")
|
||||
break
|
||||
prev = cur
|
||||
prog(t, steps - 1)
|
||||
rho, u = E.macros(f)
|
||||
return dict(N=N, Re=Re, U=U, u=cm.to_cpu(u), steps=t, frames=frames)
|
||||
|
||||
cav = run_cavity()
|
||||
print(f"Каверна: Re={cav['Re']:.0f}, N={cav['N']}, остановлено на шаге {cav['steps']}")
|
||||
|
||||
# Эталон Ghia, Ghia & Shin (1982), Re=1000.
|
||||
ghia_y = np.array([0.0000, 0.0547, 0.0625, 0.0703, 0.1016, 0.1719, 0.2813, 0.4531,
|
||||
0.5000, 0.6172, 0.7344, 0.8516, 0.9531, 0.9609, 0.9688, 0.9766, 1.0000])
|
||||
ghia_u = np.array([0.0000, -0.18109, -0.20196, -0.22220, -0.29730, -0.38289, -0.27805,
|
||||
-0.10648, -0.06080, 0.05702, 0.18719, 0.33304, 0.46604, 0.51117,
|
||||
0.57492, 0.65928, 1.0000])
|
||||
ghia_x = np.array([0.0000, 0.0625, 0.0703, 0.0781, 0.0938, 0.1563, 0.2266, 0.2344,
|
||||
0.5000, 0.8047, 0.8594, 0.9063, 0.9453, 0.9531, 0.9609, 0.9688, 1.0000])
|
||||
ghia_v = np.array([0.0000, 0.27485, 0.29012, 0.30353, 0.32627, 0.37095, 0.33075, 0.32235,
|
||||
0.02526, -0.31966, -0.42665, -0.51550, -0.39188, -0.33714, -0.27669,
|
||||
-0.21388, 0.0000])
|
||||
|
||||
Ncav = cav["N"]
|
||||
yc = (np.arange(Ncav) + 0.5)/Ncav
|
||||
xc = (np.arange(Ncav) + 0.5)/Ncav
|
||||
u_centerline = cav["u"][0, :, Ncav//2]/cav["U"]
|
||||
v_centerline = cav["u"][1, Ncav//2, :]/cav["U"]
|
||||
|
||||
fig, axs = plt.subplots(1, 3, figsize=(14, 4.4))
|
||||
spd = np.sqrt(cav["u"][0]**2 + cav["u"][1]**2)/cav["U"]
|
||||
axs[0].imshow(spd, origin="lower", cmap="viridis", extent=[0, 1, 0, 1])
|
||||
sx = np.linspace(0, 1, Ncav)
|
||||
axs[0].streamplot(sx, sx, cav["u"][0], cav["u"][1], density=1.1, color="white", linewidth=0.6)
|
||||
axs[0].set_title("Линии тока и $|\\mathbf{u}|/U$"); axs[0].set_xlabel("$x/L$"); axs[0].set_ylabel("$y/L$")
|
||||
axs[1].plot(u_centerline, yc, "-", color="#1f6feb", label="KBC")
|
||||
axs[1].plot(ghia_u, ghia_y, "o", ms=5, mfc="none", color="#d1495b", label="Ghia 1982")
|
||||
axs[1].set_title("$u_x/U$ по вертикали $x=0.5$"); axs[1].set_xlabel("$u_x/U$")
|
||||
axs[1].set_ylabel("$y/L$"); axs[1].legend()
|
||||
axs[2].plot(xc, v_centerline, "-", color="#1f6feb", label="KBC")
|
||||
axs[2].plot(ghia_x, ghia_v, "o", ms=5, mfc="none", color="#d1495b", label="Ghia 1982")
|
||||
axs[2].set_title("$u_y/U$ по горизонтали $y=0.5$"); axs[2].set_xlabel("$x/L$")
|
||||
axs[2].set_ylabel("$u_y/U$"); axs[2].legend()
|
||||
fig.suptitle("Демо-3: каверна с движущейся крышкой, Re=1000 (KBC vs Ghia)",
|
||||
y=1.04, fontweight="bold")
|
||||
cm.finish(fig, "09_cavity_ghia.png")
|
||||
|
||||
u_interp = np.interp(ghia_y, yc, u_centerline)
|
||||
rms = float(np.sqrt(np.mean((u_interp - ghia_u)**2)))
|
||||
print(f"Каверна: RMS-отклонение u_x от Ghia ~ {rms:.3f} (грубое BB → допустимо)")
|
||||
|
||||
# --- гифка становления течения -----------------------------------------------------------------
|
||||
_cf = cav["frames"]
|
||||
_vmax = max(np.sqrt(u[0]**2 + u[1]**2).max() for _, u in _cf)/cav["U"]
|
||||
|
||||
def _draw(fig, i):
|
||||
t, u = _cf[i]
|
||||
ax = fig.add_subplot(111)
|
||||
sp = np.sqrt(u[0]**2 + u[1]**2)/cav["U"]
|
||||
ax.imshow(sp, origin="lower", cmap="viridis", vmin=0, vmax=_vmax, extent=[0, 1, 0, 1])
|
||||
ax.set_title(f"Каверна Re=1000 · t={t} · $|\\mathbf{{u}}|/U$", fontsize=10)
|
||||
ax.set_xlabel("$x/L$"); ax.set_ylabel("$y/L$")
|
||||
fig.tight_layout()
|
||||
|
||||
cm.render_gif(len(_cf), _draw, "cavity_transient.gif", (4.9, 4.5), fps=24)
|
||||
|
||||
np.savez(__import__("os").path.join(cm.OUTDIR, "cavity.npz"),
|
||||
yc=yc, xc=xc, u_centerline=u_centerline, v_centerline=v_centerline,
|
||||
ghia_y=ghia_y, ghia_u=ghia_u, ghia_x=ghia_x, ghia_v=ghia_v, rms=rms,
|
||||
Re=cav["Re"], N=cav["N"], steps=cav["steps"])
|
||||
cm.write_out("cavity.txt", [
|
||||
f"N={cav['N']} Re={cav['Re']:.0f} U={cav['U']} stopped_at={cav['steps']}",
|
||||
f"rms_ux_vs_Ghia={rms:.4f}",
|
||||
"PASS каверна: качественное совпадение с Ghia (BB первого порядка)",
|
||||
])
|
||||
print("DONE (cavity)")
|
||||
@@ -0,0 +1,170 @@
|
||||
# Самопроверки математики НА КОМПОНЕНТАХ ФИНАЛЬНОЙ МОДЕЛИ (solver_2x_sdf): решётка,
|
||||
# энтропийное равновесие, KBC-проектор, предел γ=2 ≡ BGK. Результаты → out/checks.txt
|
||||
# (ноутбук цитирует их в markdown). Запуск: python checks_gpu.py [AMR_FP64=1 — жёстче пороги].
|
||||
import numpy as np
|
||||
import _common as cm
|
||||
|
||||
cp = cm.cp; xp = cm.xp; L = cm.L; E = cm.E; Coll = cm.Coll
|
||||
Q = cm.Q; CS2 = cm.CS2; TOL = cm.TOL_EXACT
|
||||
|
||||
lines = [f"backend={cm.B.BACKEND}; порог точных тождеств TOL={TOL:g}"]
|
||||
def check(name, value, tol):
|
||||
ok = value < tol
|
||||
lines.append(f"{'PASS' if ok else 'FAIL'} {name}: {value:.3e} (tol {tol:g})")
|
||||
print(lines[-1])
|
||||
assert ok, name
|
||||
return ok
|
||||
|
||||
def check_gt(name, value, floor):
|
||||
"""Для величин, которые обязаны быть НЕ малы (иначе модель выродилась)."""
|
||||
ok = value > floor
|
||||
lines.append(f"{'PASS' if ok else 'FAIL'} {name}: {value:.3e} (должно быть > {floor:g})")
|
||||
print(lines[-1])
|
||||
assert ok, name
|
||||
return ok
|
||||
|
||||
# --- 1. Решётка D2Q9: тождества весов и изотропии ---------------------------------------------
|
||||
# Допуск по типу: Wa хранится в DTYPE, и в float32 уже само Σw отличается от 1 на ~7e-9,
|
||||
# так что жёсткий 1e-12 здесь означал бы падение проверки в режиме по умолчанию.
|
||||
TOL_W = 1e-12 if cm.FP64 else 1e-6
|
||||
C = np.array(list(zip(L.Cx, L.Cy)), float)
|
||||
W = cm.to_cpu(L.Wa).astype(float)
|
||||
check("Σw − 1", abs(W.sum() - 1.0), TOL_W)
|
||||
check("|Σ w·c|", np.abs((W[:, None]*C).sum(0)).max(), TOL_W)
|
||||
check("|Σ w·c⊗c − cs²·I|",
|
||||
np.abs((W[:, None, None]*C[:, :, None]*C[:, None, :]).sum(0) - CS2*np.eye(2)).max(), TOL_W)
|
||||
assert np.all(C[list(L.OPP)] == -C), "OPP: c_opp != -c"
|
||||
lines.append("PASS OPP: c_opp = −c (точно)")
|
||||
|
||||
# --- 2. Энтропийное равновесие (product-form) точно воспроизводит ρ и ρu -----------------------
|
||||
rng = np.random.default_rng(0)
|
||||
rho_t = xp.asarray(1.0 + 0.1*rng.standard_normal((16, 16)), cm.DTYPE)
|
||||
u_t = xp.asarray(0.06*rng.standard_normal((2, 16, 16)), cm.DTYPE)
|
||||
fe = E.feq(rho_t, u_t)
|
||||
rho_m, u_m = E.macros(fe)
|
||||
check("feq: |Σf − ρ|", float(xp.abs(rho_m - rho_t).max()), TOL)
|
||||
check("feq: |u(f) − u|", float(xp.abs(u_m - u_t).max()), TOL)
|
||||
Pxx = (L.CXa[:, None, None]**2 * fe).sum(0)
|
||||
check("feq: |Σc_x²f − ρ(cs²+u_x²)| (мал при малом Ma)",
|
||||
float(xp.abs(Pxx - rho_t*(CS2 + u_t[0]**2)).max()), 5e-3)
|
||||
|
||||
# --- 3. KBC-проектор Ps: идемпотентность и состав сдвиг-моментов --------------------------------
|
||||
Ps = cm.to_cpu(L.Ps).astype(float)
|
||||
check("Ps идемпотентен: |Ps·Ps − Ps|", np.abs(Ps @ Ps - Ps).max(), 1e-5 if not cm.FP64 else 1e-12)
|
||||
# моментная матрица (как в lattice._build_projector): Δs несёт ТОЛЬКО {cx²−cy², cx·cy}
|
||||
cx, cy = C[:, 0], C[:, 1]
|
||||
M = np.zeros((Q, Q))
|
||||
M[0] = 1.0; M[1] = cx; M[2] = cy; M[3] = 3*(cx**2 + cy**2) - 2
|
||||
M[4] = cx**2 - cy**2; M[5] = cx*cy
|
||||
M[6] = cx**2*cy; M[7] = cx*cy**2; M[8] = cx**2*cy**2
|
||||
dfn = rng.standard_normal((Q, 5, 7))
|
||||
ds = np.einsum("ij,jab->iab", Ps, dfn)
|
||||
mds = np.einsum("ai,ijk->ajk", M, ds)
|
||||
other = [0, 1, 2, 3, 6, 7, 8]
|
||||
check("Ps: |моменты Δs вне {N, Πxy}|", np.abs(mds[other]).max(), 1e-4 if not cm.FP64 else 1e-10)
|
||||
lines.append("PASS состав KBC-N1: s = {Πxx−Πyy, Πxy}, h = остальное (k отщеплён точно)")
|
||||
|
||||
# --- 4. Предел γ=2: KBC-форма с γ=2 совпадает с BGK (через РЕАЛЬНЫЙ проектор модели) ------------
|
||||
f0 = fe + xp.asarray(0.01*rng.standard_normal((Q, 16, 16)), cm.DTYPE)
|
||||
r0, u0 = E.macros(f0)
|
||||
fe0 = E.feq(r0, u0)
|
||||
beta = 0.7
|
||||
dfn0 = f0 - fe0
|
||||
ds0 = Coll._project_shift(dfn0)
|
||||
fk2 = f0 - beta*(2.0*ds0 + 2.0*(dfn0 - ds0)) # KBC с γ=2
|
||||
fbgk = cm.bgk_collide(f0, fe0, beta)
|
||||
check("KBC(γ=2) ≡ BGK", float(xp.abs(fk2 - fbgk).max()), TOL)
|
||||
|
||||
# --- 5. kbc_collide сохраняет ρ и импульс -------------------------------------------------------
|
||||
fpost = Coll.kbc_collide(f0, fe0, beta)
|
||||
r1, u1 = E.macros(fpost)
|
||||
check("KBC: |Δρ| за столкновение", float(xp.abs(r1 - r0).max()), TOL)
|
||||
check("KBC: |Δ(ρu)| за столкновение", float(xp.abs(u1*r1[None] - u0*r0[None]).max()), TOL)
|
||||
|
||||
# --- 6. Стабилизатор γ ДЕЙСТВИТЕЛЬНО считается на физическом поле ------------------------------
|
||||
# Регрессия на дефект от 2026-08-14: абсолютный порог знаменателя (1e-6 в fp32) отправлял в откат
|
||||
# γ←2 77–99% узлов, т.е. модель молча вырождалась в LBGK. Проверяем на РАЗВИТОМ поле (сдвиговый
|
||||
# слой после прогрева), что откат не срабатывает и γ не залипает на 2.
|
||||
Strm = cm.Strm
|
||||
Nsl = 64; u0sl = 0.04; beta_sl = cm.nu_to_beta(u0sl*Nsl/5000.0)
|
||||
yy = xp.arange(Nsl, dtype=cm.DTYPE)[:, None] * xp.ones((1, Nsl), cm.DTYPE)
|
||||
xx = xp.ones((Nsl, 1), cm.DTYPE) * xp.arange(Nsl, dtype=cm.DTYPE)[None, :]
|
||||
uxsl = xp.where(yy <= Nsl/2, u0sl*xp.tanh(40.0*(yy/Nsl - 0.25)), u0sl*xp.tanh(40.0*(0.75 - yy/Nsl)))
|
||||
uysl = 0.05*u0sl*xp.sin(2.0*np.pi*(xx/Nsl + 0.25))
|
||||
fsl = E.feq(xp.ones((Nsl, Nsl), cm.DTYPE), cm.B.pack((uxsl, uysl)))
|
||||
for _ in range(400):
|
||||
rsl, usl = E.macros(fsl)
|
||||
fsl = Strm.stream(Coll.kbc_collide(fsl, E.feq(rsl, usl), beta_sl))
|
||||
rsl, usl = E.macros(fsl); fesl = E.feq(rsl, usl)
|
||||
dfs = fsl - fesl; dss = Coll._project_shift(dfs); dhs = dfs - dss; invs = 1.0/fesl
|
||||
den_s = (dhs*dhs*invs).sum(0); nrm_s = (dfs*dfs*invs).sum(0)
|
||||
frac_fb = float((den_s <= cm.B.GREL*nrm_s).mean())
|
||||
check("γ: доля узлов в откате γ←2 на развитом поле", frac_fb, 1e-3)
|
||||
gam = cm.kbc_gamma(fsl, fesl, beta_sl)
|
||||
# при полном вырождении в LBGK эта величина строго 0, при частичном (90% узлов) — ~0.01,
|
||||
# наблюдаемое значение ~0.13, поэтому порог 0.05 ловит регресс с запасом и не флапает
|
||||
check_gt("γ: ⟨|γ−2|⟩ — отличие от LBGK", float(xp.abs(gam - 2.0).mean()), 0.05)
|
||||
lines.append(f"INFO γ на развитом поле: ⟨γ⟩={float(gam.mean()):.4f} "
|
||||
f"[{float(gam.min()):.3f}, {float(gam.max()):.3f}], min den/‖Δ‖²="
|
||||
f"{float((den_s/xp.maximum(nrm_s, 1e-30)).min()):.2e} (порог {cm.B.GREL:g})")
|
||||
print(lines[-1])
|
||||
|
||||
# --- 7. Замкнутая формула γ ур.(17)/(39) против корня точного ур.(15) --------------------------
|
||||
# Берём один узел развитого поля и решаем Σ Δh·ln(1 + ((1−βγ)Δh − (2β−1)Δs)/feq) = 0 бисекцией.
|
||||
iy, ix = Nsl//3, Nsl//3
|
||||
fe_n = cm.to_cpu(fesl[:, iy, ix]).astype(float)
|
||||
ds_n = cm.to_cpu(dss[:, iy, ix]).astype(float); dh_n = cm.to_cpu(dhs[:, iy, ix]).astype(float)
|
||||
g_formula = float(cm.to_cpu(gam[iy, ix]))
|
||||
def _crit(g):
|
||||
return float((dh_n*np.log(1.0 + ((1.0 - beta_sl*g)*dh_n - (2.0*beta_sl - 1.0)*ds_n)/fe_n)).sum())
|
||||
lo, hi = g_formula - 2.0, g_formula + 2.0
|
||||
while _crit(lo)*_crit(hi) > 0 and hi - lo < 40:
|
||||
lo -= 1.0; hi += 1.0
|
||||
for _ in range(200):
|
||||
mid = 0.5*(lo + hi)
|
||||
if _crit(lo)*_crit(mid) <= 0: hi = mid
|
||||
else: lo = mid
|
||||
check("γ: |формула − точный корень ур.(15)|", abs(g_formula - 0.5*(lo + hi)), 5e-2)
|
||||
|
||||
# --- 8. Гидродинамический предел: ν по затуханию сдвиговой волны --------------------------------
|
||||
# Единственная проверка, которая ловит подмену оператора: LBGK и KBC дают одну ν, но неверная
|
||||
# релаксация сдвиг-моментов сразу видна здесь.
|
||||
Nw = 48; kw = 2.0*np.pi/Nw; NSW = 1500
|
||||
sinw = xp.sin(kw*xp.arange(Nw, dtype=cm.DTYPE))
|
||||
for tau_w in (0.6, 1.0):
|
||||
beta_w = 1.0/(2.0*tau_w); nu_th = CS2*(tau_w - 0.5)
|
||||
# амплитуда 1e-2 (Ma≈0.017, режим ещё линейный): в float32 при 1e-4 сигнал тонет в округлении
|
||||
uxw = 0.01*sinw[:, None]*xp.ones((1, 4), cm.DTYPE)
|
||||
fw = E.feq(xp.ones((Nw, 4), cm.DTYPE), cm.B.pack((uxw, xp.zeros_like(uxw))))
|
||||
amps = []
|
||||
for _ in range(NSW + 1):
|
||||
rw, uw = E.macros(fw)
|
||||
amps.append(abs(float((uw[0][:, 0]*sinw).sum()))) # проекция на моду, без abs поэлементно
|
||||
fw = Strm.stream(Coll.kbc_collide(fw, E.feq(rw, uw), beta_w))
|
||||
a = np.array(amps); i0 = NSW//3
|
||||
rate = -np.polyfit(np.arange(i0, NSW + 1), np.log(a[i0:]), 1)[0]
|
||||
# допуск 1e-2 покрывает решёточную дисперсию O(k²) (k=0.131 → ~3e-3)
|
||||
check(f"ν(τ={tau_w}): отн. ошибка затухания сдвиговой волны", abs(rate/(kw*kw)/nu_th - 1.0), 1e-2)
|
||||
|
||||
# --- 9. ГУ канала: вход и выход не связаны заворотом roll (включая углы) ------------------------
|
||||
# Регрессия на дефект от 2026-08-14: Zou-He стоял на срезе 1:-1, и в 4 углах stream() через roll
|
||||
# сносил популяции прямо из столбца выхода в столбец входа.
|
||||
import boundary as Bnd
|
||||
Ny_b, Nx_b = 24, 32
|
||||
fb = E.feq(xp.ones((Ny_b, Nx_b), cm.DTYPE), 0.02*xp.ones((2, Ny_b, Nx_b), cm.DTYPE))
|
||||
fb = fb*(1.0 + xp.asarray(0.05*rng.standard_normal(fb.shape), cm.DTYPE))
|
||||
post_b = fb.copy()
|
||||
uin_b = xp.zeros((2, Ny_b, 1), cm.DTYPE); uin_b[0] = 0.05
|
||||
fs_b = Bnd.channel_bc(Bnd.free_slip_walls(Strm.stream(post_b)), uin_b, outlet_uy="zero")
|
||||
r_in = fs_b[:, :, 0].sum(0)
|
||||
ux_in = (L.CXa[:, None]*fs_b[:, :, 0]).sum(0)/r_in
|
||||
check("ГУ: |u_x(вход) − заданной| по ВСЕМ рядам, вкл. углы", float(xp.abs(ux_in - 0.05).max()), TOL)
|
||||
check("ГУ: |ρ(выход) − 1| по ВСЕМ рядам, вкл. углы",
|
||||
float(xp.abs(fs_b[:, :, -1].sum(0) - 1.0).max()), TOL)
|
||||
leak = min(float(xp.abs(fs_b[i, row, 0] - post_b[i, row, -1]).max())
|
||||
for i in (1, 5, 8) for row in (0, -1))
|
||||
check_gt("ГУ: расхождение углового f с популяцией выхода (заворот подавлен)", leak, 1e-6)
|
||||
|
||||
lines.append("=== Все самопроверки пройдены ===")
|
||||
print(lines[-1])
|
||||
cm.write_out("checks.txt", lines)
|
||||
@@ -0,0 +1,153 @@
|
||||
# Статические иллюстрации (НЕ симуляции): стенсили D2Q9/D3Q27, блок-схема шага KBC,
|
||||
# схема блочного измельчения. Чистый numpy+matplotlib — НЕ импортирует CuPy и потому
|
||||
# исполним и локально (единственный скрипт demos_gpu без GPU).
|
||||
# Схема измельчения использует фон |ω| из out/obstacle_circle_field.npz (его пишет
|
||||
# obstacle_gpu.py на сервере); без него рисуется нейтральный фон.
|
||||
# Артефакты: figures/01_stencil_d2q9.png, 06_algorithm_flow.png, 11c_refinement_schematic.png,
|
||||
# 11_d3q27.png. Запуск: python figures_static.py
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as mpatches
|
||||
from matplotlib.lines import Line2D
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
THEORY = os.path.dirname(HERE)
|
||||
FIGDIR = os.path.join(THEORY, "figures"); os.makedirs(FIGDIR, exist_ok=True)
|
||||
OUTDIR = os.path.join(HERE, "out")
|
||||
|
||||
plt.rcParams.update({
|
||||
"figure.dpi": 110, "savefig.dpi": 120, "font.size": 11,
|
||||
"axes.titlesize": 13, "axes.titleweight": "bold",
|
||||
"axes.grid": True, "grid.alpha": 0.25, "axes.axisbelow": True,
|
||||
"figure.facecolor": "white", "axes.facecolor": "#fbfbfd",
|
||||
})
|
||||
|
||||
def finish(fig, name):
|
||||
fig.savefig(os.path.join(FIGDIR, name), bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
print("PNG:", name)
|
||||
|
||||
# D2Q9 (та же нумерация, что в solver_2x_sdf/lattice.py)
|
||||
CX = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1], float)
|
||||
CY = np.array([0, 0, 1, 0, -1, 1, 1, -1, -1], float)
|
||||
W = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])
|
||||
CS2 = 1.0/3.0
|
||||
|
||||
# --- 1. Стенсиль D2Q9 ---------------------------------------------------------------------------
|
||||
fig, ax = plt.subplots(figsize=(5.2, 5.2))
|
||||
tier_color = {4/9: "#d1495b", 1/9: "#2e7d32", 1/36: "#1f6feb"}
|
||||
for i in range(9):
|
||||
col = tier_color[W[i]]
|
||||
if i == 0:
|
||||
ax.plot(0, 0, "o", ms=18, color=col, zorder=3)
|
||||
ax.annotate(f"$w_0={W[i]:.3f}$", (0, 0), textcoords="offset points",
|
||||
xytext=(8, 8), fontsize=9)
|
||||
else:
|
||||
ax.annotate("", xy=(CX[i], CY[i]), xytext=(0, 0),
|
||||
arrowprops=dict(arrowstyle="-|>", lw=1.5 + 40*W[i], color=col))
|
||||
ax.annotate(f"$c_{{{i}}}$", (CX[i], CY[i]), textcoords="offset points",
|
||||
xytext=(6*np.sign(CX[i] + 0.1), 6*np.sign(CY[i] + 0.1)), fontsize=9)
|
||||
ax.set_xlim(-1.6, 1.6); ax.set_ylim(-1.6, 1.6); ax.set_aspect("equal")
|
||||
ax.set_title("Решётка D2Q9: дискретные скорости $\\mathbf{c}_i$ и веса $w_i$")
|
||||
ax.set_xlabel("$c_x$ [lu/ts]"); ax.set_ylabel("$c_y$ [lu/ts]")
|
||||
ax.legend(handles=[Line2D([0], [0], color=c, lw=3, label=f"$w={k:.4f}$")
|
||||
for k, c in tier_color.items()], loc="upper right", fontsize=8)
|
||||
finish(fig, "01_stencil_d2q9.png")
|
||||
|
||||
# --- 2. Блок-схема шага KBC ---------------------------------------------------------------------
|
||||
fig, ax = plt.subplots(figsize=(4.3, 6.2))
|
||||
ax.set_axis_off()
|
||||
steps_txt = [
|
||||
("Макро-моменты\n$\\rho=\\sum f_i,\\ \\rho\\mathbf{u}=\\sum \\mathbf{c}_i f_i$", "#e8f0fe"),
|
||||
("Равновесие $f_i^{eq}$\n(энтропийное)", "#e6f4ea"),
|
||||
("Неравновесие\n$\\Delta s=P_s(f-f^{eq}),\\ \\Delta h=(f-f^{eq})-\\Delta s$", "#fff4e5"),
|
||||
("Стабилизатор $\\gamma$\n$=1/\\beta-(2-1/\\beta)\\frac{\\langle\\Delta s|\\Delta h\\rangle}{\\langle\\Delta h|\\Delta h\\rangle}$", "#fde7ef"),
|
||||
("Столкновение\n$f\\leftarrow f-\\beta(2\\Delta s+\\gamma\\Delta h)$", "#f3e8fd"),
|
||||
("Перенос\n$f_i(\\mathbf{x}+\\mathbf{c}_i)\\leftarrow f_i(\\mathbf{x})$", "#e8f0fe"),
|
||||
("Граничные условия", "#eceff1"),
|
||||
]
|
||||
y = 0.95
|
||||
for txt, col in steps_txt:
|
||||
ax.add_patch(plt.Rectangle((0.05, y - 0.1), 0.9, 0.085, transform=ax.transAxes,
|
||||
facecolor=col, edgecolor="#555", lw=1.2))
|
||||
ax.text(0.5, y - 0.057, txt, transform=ax.transAxes, ha="center", va="center", fontsize=8.5)
|
||||
if y > 0.2:
|
||||
ax.annotate("", xy=(0.5, y - 0.118), xytext=(0.5, y - 0.1), xycoords="axes fraction",
|
||||
arrowprops=dict(arrowstyle="-|>", color="#555"))
|
||||
y -= 0.13
|
||||
ax.set_title("Шаг KBC")
|
||||
finish(fig, "06_algorithm_flow.png")
|
||||
|
||||
# --- 3. Схема блочного измельчения (фон — |ω| цилиндра, если есть артефакт) ----------------------
|
||||
def _vorticity(u):
|
||||
duy_dx = (np.roll(u[1], -1, 1) - np.roll(u[1], 1, 1))*0.5
|
||||
dux_dy = (np.roll(u[0], -1, 0) - np.roll(u[0], 1, 0))*0.5
|
||||
return duy_dx - dux_dy
|
||||
|
||||
field_path = os.path.join(OUTDIR, "obstacle_circle_field.npz")
|
||||
schematic_path = os.path.join(FIGDIR, "11c_refinement_schematic.png")
|
||||
if not os.path.exists(field_path) and os.path.exists(schematic_path):
|
||||
# без серверного фона не затираем уже существующую схему с |ω|-подложкой
|
||||
print("(нет out/obstacle_circle_field.npz — оставляю существующий 11c_refinement_schematic.png; "
|
||||
"запустите obstacle_gpu.py на сервере и повторите)")
|
||||
else:
|
||||
fig, ax = plt.subplots(figsize=(11, 5.6))
|
||||
if os.path.exists(field_path):
|
||||
d = np.load(field_path)
|
||||
u = d["u"]; solid = d["solid"]
|
||||
Nx, Ny = int(d["Nx"]), int(d["Ny"]); cx, cy, D = int(d["cx"]), int(d["cy"]), int(d["D"])
|
||||
vmag = np.where(solid, np.nan, np.abs(_vorticity(u)))
|
||||
ax.imshow(vmag, origin="lower", cmap="inferno", vmax=np.nanpercentile(vmag, 99))
|
||||
else:
|
||||
Nx, Ny, cx, cy, D = 150, 72, 37, 36, 14
|
||||
ax.add_patch(plt.Circle((cx, cy), D/2, color="#37474f"))
|
||||
ax.set_facecolor("#10101a")
|
||||
|
||||
def _grid_block(x0, y0, w, h, step, color, label):
|
||||
ax.add_patch(mpatches.Rectangle((x0, y0), w, h, fill=False, edgecolor=color, lw=2.2))
|
||||
for xx in np.arange(x0, x0 + w + 0.1, step):
|
||||
ax.plot([xx, xx], [y0, y0 + h], color=color, lw=0.4, alpha=0.45)
|
||||
for yy in np.arange(y0, y0 + h + 0.1, step):
|
||||
ax.plot([x0, x0 + w], [yy, yy], color=color, lw=0.4, alpha=0.45)
|
||||
ax.text(x0 + 1, y0 + h - 4, label, color=color, fontsize=8, weight="bold")
|
||||
|
||||
_grid_block(0, 0, Nx - 1, Ny - 1, 16, "#4fc3f7", "уровень 0: Δx (крупный)")
|
||||
_grid_block(cx - 1.5*D, cy - 1.9*D, 6.5*D, 3.8*D, 8, "#aed581", "уровень 1: Δx/2 (тело+след)")
|
||||
_grid_block(cx - 1.1*D, cy - 1.3*D, 2.6*D, 2.6*D, 4, "#ff8a65", "уровень 2: Δx/4 (у тела)")
|
||||
ax.set_xlim(0, Nx - 1); ax.set_ylim(0, Ny - 1)
|
||||
ax.set_title("Схема блочного измельчения вокруг тела (поверх $|\\omega|$): "
|
||||
"сетка мельче там, где градиенты резче")
|
||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]")
|
||||
finish(fig, "11c_refinement_schematic.png")
|
||||
|
||||
# --- 4. Стенсиль D3Q27 --------------------------------------------------------------------------
|
||||
C3 = np.array([[x, y, z] for x in (-1, 0, 1) for y in (-1, 0, 1) for z in (-1, 0, 1)], dtype=float)
|
||||
nz = (C3 != 0).sum(1)
|
||||
W3 = np.select([nz == 0, nz == 1, nz == 2, nz == 3], [8/27, 2/27, 1/54, 1/216])
|
||||
assert np.isclose(W3.sum(), 1.0)
|
||||
assert np.allclose((W3[:, None, None]*C3[:, :, None]*C3[:, None, :]).sum(0), CS2*np.eye(3))
|
||||
print("D3Q27: Σw=1 и Σw c⊗c = cs²·I — OK; направлений:", len(C3))
|
||||
|
||||
fig = plt.figure(figsize=(6.4, 5.6))
|
||||
ax = fig.add_subplot(111, projection="3d")
|
||||
tier3 = {8/27: ("#d1495b", "покой"), 2/27: ("#2e7d32", "оси (×6)"),
|
||||
1/54: ("#1f6feb", "рёбра (×12)"), 1/216: ("#9c27b0", "углы (×8)")}
|
||||
for wv, (col, lab) in tier3.items():
|
||||
m = np.isclose(W3, wv)
|
||||
ax.scatter(C3[m, 0], C3[m, 1], C3[m, 2], s=70 + 5000*wv, color=col,
|
||||
label=f"{lab}, w={wv:.4f}", depthshade=True, edgecolors="k", linewidths=0.4)
|
||||
ax.set_xlabel("$c_x$"); ax.set_ylabel("$c_y$"); ax.set_zlabel("$c_z$")
|
||||
ax.set_title("Решётка D3Q27 (цель реализации)")
|
||||
ax.legend(loc="upper left", fontsize=7, bbox_to_anchor=(0.0, 1.0))
|
||||
finish(fig, "11_d3q27.png")
|
||||
|
||||
print("DONE (static figures)")
|
||||
@@ -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)")
|
||||
@@ -0,0 +1,109 @@
|
||||
# Демо-2: двойной сдвиговый слой (Minion–Brown) на недоразрешённой сетке — устойчивость
|
||||
# BGK (poly-равновесие) против KBC (entropic + γ): BGK разрушается, KBC держит (implicit LES).
|
||||
# Артефакты: figures/08_shear_stability.png, anim/shear_bgk_vs_kbc.gif, out/shear.txt.
|
||||
# Запуск: python shear_gpu.py
|
||||
import math
|
||||
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
|
||||
|
||||
def init_shear(N, U0=0.04, delta=0.05, kappa=80.0):
|
||||
xs = cp.arange(N, dtype=cm.DTYPE)/N
|
||||
X, Yc = cp.meshgrid(xs, xs)
|
||||
ux = cp.where(Yc <= 0.5, U0*cp.tanh(kappa*(Yc - 0.25)), U0*cp.tanh(kappa*(0.75 - Yc)))
|
||||
uy = delta*U0*cp.sin(2.0*math.pi*(X + 0.25))
|
||||
return cm.B.pack((ux.astype(cm.DTYPE), uy.astype(cm.DTYPE)))
|
||||
|
||||
def run_shear(scheme, N=128, U0=0.04, nu=1.7e-4, steps=8000, rec=100, frame_every=33):
|
||||
u0 = init_shear(N, U0=U0)
|
||||
rho = cp.ones((N, N), cm.DTYPE)
|
||||
beta = cm.nu_to_beta(nu)
|
||||
f = E.feq(rho, u0) if scheme == "kbc" else cm.feq_poly(rho, u0)
|
||||
ts, umax, frames = [], [], []
|
||||
blew_up_at = None
|
||||
last_u = cm.to_cpu(u0)
|
||||
prog = cm.Viz.make_progress(scheme)
|
||||
for t in range(steps + 1):
|
||||
rho, u = E.macros(f)
|
||||
um = float(xp.abs(u).max())
|
||||
if not math.isfinite(um) or um > 0.5:
|
||||
blew_up_at = t
|
||||
print(f"\n{scheme}: разрушился на шаге {t}")
|
||||
break
|
||||
if t % frame_every == 0:
|
||||
frames.append((t, cm.to_cpu(u)))
|
||||
if scheme == "kbc":
|
||||
f = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
||||
else:
|
||||
f = cm.bgk_collide(f, cm.feq_poly(rho, u), beta)
|
||||
f = Strm.stream(f)
|
||||
last_u = cm.to_cpu(u)
|
||||
if t % rec == 0:
|
||||
ts.append(t); umax.append(um)
|
||||
prog(t, steps)
|
||||
gamma = None
|
||||
if scheme == "kbc": # поле γ на финальном состоянии — для панели
|
||||
r, u = E.macros(f)
|
||||
gamma = cm.to_cpu(cm.kbc_gamma(f, E.feq(r, u), beta))
|
||||
return dict(scheme=scheme, ts=np.array(ts), umax=np.array(umax), frames=frames,
|
||||
u=last_u, gamma=gamma, blew_up_at=blew_up_at, N=N, U0=U0, nu=nu)
|
||||
|
||||
Re_shear = 0.04*128/1.7e-4
|
||||
print(f"Сдвиговый слой: Re ~ {Re_shear:.0f}, β ~ {cm.nu_to_beta(1.7e-4):.4f}")
|
||||
res_bgk = run_shear("bgk")
|
||||
res_kbc = run_shear("kbc")
|
||||
print("BGK:", (f"разрушился на t={res_bgk['blew_up_at']}" if res_bgk["blew_up_at"] else "устойчив"))
|
||||
print("KBC:", (f"разрушился на t={res_kbc['blew_up_at']}" if res_kbc["blew_up_at"] else "устойчив"))
|
||||
assert res_kbc["blew_up_at"] is None, "KBC должен оставаться устойчивым"
|
||||
|
||||
# --- панель: ω(KBC), поле γ−2, max|u|(t) -------------------------------------------------------
|
||||
fig, axs = plt.subplots(1, 3, figsize=(13.5, 4.0))
|
||||
om_k = vorticity(res_kbc["u"])
|
||||
im0 = axs[0].imshow(om_k, origin="lower", cmap="RdBu_r")
|
||||
axs[0].set_title("KBC: завихренность (устойчиво)")
|
||||
axs[0].set_xlabel("$x$ [lu]"); axs[0].set_ylabel("$y$ [lu]")
|
||||
plt.colorbar(im0, ax=axs[0], fraction=0.046)
|
||||
im1 = axs[1].imshow(res_kbc["gamma"] - 2.0, origin="lower", cmap="magma")
|
||||
axs[1].set_title("Поле стабилизатора $\\gamma-2$")
|
||||
axs[1].set_xlabel("$x$ [lu]"); axs[1].set_ylabel("$y$ [lu]")
|
||||
plt.colorbar(im1, ax=axs[1], fraction=0.046)
|
||||
axs[2].plot(res_kbc["ts"], res_kbc["umax"], color="#2e7d32", label="KBC")
|
||||
axs[2].plot(res_bgk["ts"], res_bgk["umax"], color="#d1495b", label="BGK")
|
||||
if res_bgk["blew_up_at"]:
|
||||
axs[2].axvline(res_bgk["blew_up_at"], color="#d1495b", ls="--", alpha=0.6)
|
||||
axs[2].set_title("$\\max|\\mathbf{u}|$ во времени")
|
||||
axs[2].set_xlabel("$t$ [ts]"); axs[2].set_ylabel("$\\max|\\mathbf{u}|$ [lu/ts]"); axs[2].legend()
|
||||
fig.suptitle(f"Демо-2: сдвиговый слой, Re~{Re_shear:.0f} (BGK разрушается, KBC держит)",
|
||||
y=1.04, fontweight="bold")
|
||||
cm.finish(fig, "08_shear_stability.png")
|
||||
|
||||
# --- гифка: BGK (слева, замирает с пометкой) vs KBC (справа) -----------------------------------
|
||||
_kf, _bf = res_kbc["frames"], res_bgk["frames"]
|
||||
_om0s = np.percentile(np.abs(vorticity(_kf[len(_kf)//2][1])), 99.5)
|
||||
|
||||
def _draw(fig, i):
|
||||
ax1 = fig.add_subplot(1, 2, 1)
|
||||
ax2 = fig.add_subplot(1, 2, 2)
|
||||
dead = i >= len(_bf)
|
||||
tb, ub = _bf[min(i, len(_bf) - 1)]
|
||||
ax1.imshow(vorticity(ub), origin="lower", cmap="RdBu_r", vmin=-_om0s, vmax=_om0s)
|
||||
ax1.set_title("BGK — разрушился" if dead else f"BGK · t={tb}",
|
||||
color="red" if dead else "black", fontsize=10)
|
||||
tk, uk = _kf[i]
|
||||
ax2.imshow(vorticity(uk), origin="lower", cmap="RdBu_r", vmin=-_om0s, vmax=_om0s)
|
||||
ax2.set_title(f"KBC · t={tk}", fontsize=10)
|
||||
for a in (ax1, ax2):
|
||||
a.set_xticks([]); a.set_yticks([])
|
||||
fig.tight_layout()
|
||||
|
||||
cm.render_gif(len(_kf), _draw, "shear_bgk_vs_kbc.gif", (8.4, 4.3), fps=24)
|
||||
|
||||
cm.write_out("shear.txt", [
|
||||
f"N=128 U0=0.04 nu=1.7e-4 Re~{Re_shear:.0f} steps=8000",
|
||||
f"BGK_blew_up_at={res_bgk['blew_up_at']}",
|
||||
f"KBC_stable={res_kbc['blew_up_at'] is None}",
|
||||
"PASS KBC устойчив там, где BGK разрушается",
|
||||
])
|
||||
print("DONE (shear)")
|
||||
@@ -0,0 +1,108 @@
|
||||
# Демо-1: вихрь Тейлора–Грина (валидация вязкости и H-теоремы) на компонентах solver_2x_sdf.
|
||||
# Аналитика: E(t) = E0·exp(−4νk²t). Меряем ν по наклону ln E(t) — должен совпасть с заданным.
|
||||
# Артефакты: figures/07_taylor_green.png, figures/07b_tgv_decay_slices.png, anim/tgv_decay.gif,
|
||||
# out/tgv.txt. Запуск: python tgv_gpu.py
|
||||
import math
|
||||
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
|
||||
|
||||
def run_tgv(N=96, U0=0.04, nu=0.0125, steps=10000, rec=50, n_snap=10, frame_every=42):
|
||||
k = 2.0*math.pi/N
|
||||
xs = cp.arange(N, dtype=cm.DTYPE)
|
||||
X, Yc = cp.meshgrid(xs, xs) # X — ось 1, Yc — ось 0
|
||||
ux = -U0*cp.cos(k*X)*cp.sin(k*Yc)
|
||||
uy = U0*cp.sin(k*X)*cp.cos(k*Yc)
|
||||
rho = cp.ones((N, N), cm.DTYPE)
|
||||
f = E.feq(rho, cm.B.pack((ux.astype(cm.DTYPE), uy.astype(cm.DTYPE))))
|
||||
beta = cm.nu_to_beta(nu)
|
||||
snap_at = sorted(set(int(round(v)) for v in np.linspace(0, steps, n_snap)))
|
||||
snaps, frames, ts, KE, Hs = [], [], [], [], []
|
||||
prog = cm.Viz.make_progress("tgv")
|
||||
for t in range(steps + 1):
|
||||
rho, u = E.macros(f)
|
||||
f = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
||||
f = Strm.stream(f) # периодический перенос — ровно случай TGV
|
||||
if (t in snap_at) or (t % rec == 0) or (t % frame_every == 0):
|
||||
_, uu = E.macros(f)
|
||||
if t in snap_at:
|
||||
snaps.append((t, cm.to_cpu(uu)))
|
||||
if t % frame_every == 0:
|
||||
frames.append((t, cm.to_cpu(uu)))
|
||||
if t % rec == 0:
|
||||
ts.append(t)
|
||||
KE.append(float((0.5*(uu[0]**2 + uu[1]**2)).mean()))
|
||||
Hs.append(float(cm.H_function(f).mean()))
|
||||
prog(t, steps)
|
||||
return dict(N=N, k=k, nu=nu, U0=U0, ts=np.array(ts), KE=np.array(KE),
|
||||
H=np.array(Hs), u=cm.to_cpu(E.macros(f)[1]), snaps=snaps, frames=frames)
|
||||
|
||||
tgv = run_tgv()
|
||||
mask = tgv["KE"] > 0
|
||||
slope = np.polyfit(tgv["ts"][mask], np.log(tgv["KE"][mask]), 1)[0]
|
||||
nu_meas = -slope/(4.0*tgv["k"]**2)
|
||||
rel_err = abs(nu_meas - tgv["nu"])/tgv["nu"]
|
||||
print(f"TGV: ν задано={tgv['nu']:.5f}, ν измерено={nu_meas:.5f}, отн. ошибка={rel_err*100:.2f}%")
|
||||
assert rel_err < 0.06, "TGV: измеренная вязкость отклонилась слишком сильно"
|
||||
dH = np.diff(tgv["H"])
|
||||
print(f"TGV: H-функция монотонно невозрастает: max(ΔH)={dH.max():.2e}")
|
||||
|
||||
# --- статическая панель: ω, затухание энергии, H(t) -------------------------------------------
|
||||
fig, axs = plt.subplots(1, 3, figsize=(13.5, 4.0))
|
||||
om = vorticity(tgv["u"])
|
||||
im = axs[0].imshow(om, origin="lower", cmap="RdBu_r")
|
||||
axs[0].set_title("Завихренность $\\omega$ (TGV, срез)")
|
||||
axs[0].set_xlabel("$x$ [lu]"); axs[0].set_ylabel("$y$ [lu]")
|
||||
plt.colorbar(im, ax=axs[0], fraction=0.046)
|
||||
axs[1].semilogy(tgv["ts"], tgv["KE"], "o", ms=3, label="KBC (измерено)")
|
||||
axs[1].semilogy(tgv["ts"], tgv["KE"][0]*np.exp(-4*tgv["nu"]*tgv["k"]**2*tgv["ts"]),
|
||||
"-", color="#d1495b", label="$E_0 e^{-4\\nu k^2 t}$ (аналитика)")
|
||||
axs[1].set_title("Затухание кин. энергии"); axs[1].set_xlabel("$t$ [ts]")
|
||||
axs[1].set_ylabel("$E(t)$"); axs[1].legend()
|
||||
axs[2].plot(tgv["ts"], tgv["H"], color="#2e7d32")
|
||||
axs[2].set_title("H-функция (энтропия) во времени")
|
||||
axs[2].set_xlabel("$t$ [ts]"); axs[2].set_ylabel("$\\langle H\\rangle$")
|
||||
fig.suptitle("Демо-1: вихрь Тейлора–Грина (KBC, D2Q9)", y=1.04, fontweight="bold")
|
||||
cm.finish(fig, "07_taylor_green.png")
|
||||
|
||||
# --- 10 срезов распада с единой шкалой цвета ---------------------------------------------------
|
||||
snaps = tgv["snaps"]
|
||||
om0 = np.abs(vorticity(snaps[0][1])).max()
|
||||
KE0 = 0.5*np.mean(snaps[0][1][0]**2 + snaps[0][1][1]**2)
|
||||
fig, axs = plt.subplots(2, 5, figsize=(15, 6.3), constrained_layout=True)
|
||||
im = None
|
||||
for ax, (tt, uu) in zip(axs.ravel(), snaps):
|
||||
im = ax.imshow(vorticity(uu), origin="lower", cmap="RdBu_r", vmin=-om0, vmax=om0)
|
||||
KEf = 0.5*np.mean(uu[0]**2 + uu[1]**2)/KE0
|
||||
ax.set_title(f"t={tt} · $E/E_0$={KEf:.2f}", fontsize=10)
|
||||
ax.set_xticks([]); ax.set_yticks([])
|
||||
fig.colorbar(im, ax=axs, fraction=0.025, pad=0.01, label="завихренность $\\omega$ [1/ts]")
|
||||
fig.suptitle("Распад вихря Тейлора–Грина: завихренность на равных интервалах времени "
|
||||
"(единая шкала цвета)", fontweight="bold")
|
||||
cm.finish(fig, "07b_tgv_decay_slices.png")
|
||||
|
||||
# --- гифка затухания ---------------------------------------------------------------------------
|
||||
_fr = tgv["frames"]
|
||||
_om0 = np.abs(vorticity(_fr[0][1])).max()
|
||||
_KE0 = 0.5*np.mean(_fr[0][1][0]**2 + _fr[0][1][1]**2)
|
||||
|
||||
def _draw(fig, i):
|
||||
t, u = _fr[i]
|
||||
ax = fig.add_subplot(111)
|
||||
ax.imshow(vorticity(u), origin="lower", cmap="RdBu_r", vmin=-_om0, vmax=_om0)
|
||||
ke = 0.5*np.mean(u[0]**2 + u[1]**2)/_KE0
|
||||
ax.set_title(f"Тейлор–Грин · t={t} · $E/E_0$={ke:.2f}", fontsize=10)
|
||||
ax.set_xticks([]); ax.set_yticks([])
|
||||
fig.tight_layout()
|
||||
|
||||
cm.render_gif(len(_fr), _draw, "tgv_decay.gif", (4.6, 4.4), fps=24)
|
||||
|
||||
cm.write_out("tgv.txt", [
|
||||
f"N={tgv['N']} U0={tgv['U0']} steps=10000",
|
||||
f"nu_given={tgv['nu']:.5f} nu_measured={nu_meas:.5f} rel_err_pct={rel_err*100:.2f}",
|
||||
f"H_monotone_max_dH={dH.max():.3e}",
|
||||
"PASS TGV: вязкость по затуханию энергии и H-теорема",
|
||||
])
|
||||
print("DONE (tgv)")
|
||||
Reference in New Issue
Block a user