Files
CFDManager/docs/theory/solver_2x_sdf/visualization.py
T
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

93 lines
5.9 KiB
Python
Raw 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.
# Визуализация (host): гифка |ω| с наложением тонкого патча L1 и прогресс-бар.
# matplotlib/PIL работают только с host-массивами — кадры уже перенесены на host в solver.run().
import os
import time as _time
import numpy as np
def make_progress(label, width=32, every=0.1):
"""ASCII-шкала прогресса по выполненным шагам из заданных (обновление на месте через \\r)."""
st = {"t0": _time.perf_counter(), "last": 0.0}
def cb(t, total):
now = _time.perf_counter(); done = (t >= total)
if not done and (now - st["last"] < every):
return
st["last"] = now; frac = (t + 1) / (total + 1)
filled = int(round(width * frac)); bar = "#"*filled + "-"*(width - filled)
el = now - st["t0"]; eta = (el/frac - el) if frac > 0 else 0.0
print(f"\r {label:8}[{bar}] {frac*100:5.1f}% ({t}/{total}) {el:5.1f}s ETA {eta:5.1f}s",
end=("\n" if done else ""), flush=True)
return cb
def _vort(u):
return (np.roll(u[1], -1, 1) - np.roll(u[1], 1, 1))*0.5 - (np.roll(u[0], -1, 0) - np.roll(u[0], 1, 0))*0.5
def save_series_panel(res, a, cfg, path):
"""Готовая png-панель для ноутбука (он код не исполняет — только вставляет картинки):
(1) ряды Cd(t)/Cl(t) + редкие точки кросс-чека ∮σ·n ds; (2) Cm(t) — контроль симметрии;
(3) спектр зонда u_y с пиком St; (4) ⟨ρ⟩(t) — контроль дрейфа массы."""
import backend as B
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
Cd = a["Cd_s"]; Cl = a["Cl_s"]; n = len(Cd); h0 = a["h0"]; t = np.arange(n)
fig, axs = plt.subplots(2, 2, figsize=(12.5, 7.2), dpi=110)
ax = axs[0, 0]
ax.plot(t, Cd, lw=0.7, color="#d32f2f", label=f"Cd (GMEM), ⟨Cd⟩={a['Cd']:.3f}")
ax.plot(t, Cl, lw=0.7, color="#1976d2", label=f"Cl (GMEM), rms={a['Clrms']:.3f}")
if "Cd_st_s" in a:
K = a["stress_every"]; ts = np.arange(len(a["Cd_st_s"])) * K
ax.plot(ts, a["Cd_st_s"], ".", ms=3, color="#ff8f00", label=f"Cd ∮σ·n, ⟨⟩={a['Cd_st']:.3f}")
ax.plot(ts, a["Cl_st_s"], ".", ms=3, color="#4fc3f7", label="Cl ∮σ·n")
ax.axvline(h0, color="gray", lw=0.8, ls="--")
ax.set_xlabel("шаг L0"); ax.set_ylabel("Cd, Cl"); ax.legend(fontsize=8, loc="upper right")
ax.set_title("Сила: GMEM (линии) vs ∮σ·n ds (точки)", fontsize=10)
ax = axs[0, 1]
if "Cm_s" in a:
ax.plot(t, a["Cm_s"], lw=0.7, color="#388e3c")
ax.axhline(0.0, color="gray", lw=0.8)
ax.set_title(f"Cm(t): ⟨Cm⟩={a['Cm']:.5f} (контроль симметрии, ожид. ≈0)", fontsize=10)
ax.set_xlabel("шаг L0"); ax.set_ylabel("Cm")
ax = axs[1, 0]
ax.plot(a["freqs"]*cfg.D/cfg.U, a["amp"], lw=0.9, color="#7b1fa2")
ax.axvline(a["St"], color="#d32f2f", lw=1.0, ls="--", label=f"St={a['St']:.3f}")
ax.set_xlim(0, 0.6); ax.set_xlabel("St = f·D/U"); ax.set_ylabel("|FFT(u_y)|")
ax.legend(fontsize=9); ax.set_title("Спектр зонда в следе (вторая половина ряда)", fontsize=10)
ax = axs[1, 1]
rho = B.to_cpu(res["rho"]) if not isinstance(res["rho"], np.ndarray) else res["rho"]
ax.plot(np.arange(len(rho)), rho, lw=0.9, color="#5d4037")
ax.axhline(1.0, color="gray", lw=0.8)
ax.set_xlabel("шаг L0"); ax.set_ylabel("⟨ρ⟩")
ax.set_title(f"⟨ρ⟩(t): дрейф массы (финально {float(rho[-1]):.4f})", fontsize=10)
fig.suptitle(f"2×+SDF · Ny={cfg.Ny} (β={cfg.D/cfg.Ny:.3f}) · Re={cfg.Re:g} · {len(rho)-1} шагов",
fontsize=11)
fig.tight_layout(rect=(0, 0, 1, 0.96))
os.makedirs(os.path.dirname(path), exist_ok=True)
fig.savefig(path, bbox_inches="tight"); plt.close(fig)
print("saved", os.path.basename(path))
def save_gif(res, geo, cfg, path, fps=24):
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
from PIL import Image
s0 = geo.solid0_np; s1 = geo.solid1_np
Nx, Ny = cfg.Nx, cfg.Ny; ax1, bx1, ay1, by1 = cfg.ax1, cfg.bx1, cfg.ay1, cfg.by1
frames = res["frames"]
vm = np.nanpercentile(np.abs(np.where(s0, np.nan, _vort(frames[len(frames)//2][1]))), 99.0)
cm = plt.get_cmap("inferno").copy(); cm.set_bad("white"); pil = []
for (t, u0, u1) in frames:
fig = plt.figure(figsize=(11.2, 5.7), dpi=96); ax = fig.add_subplot(111)
ax.imshow(np.abs(np.where(s0, np.nan, _vort(u0))), origin="lower", cmap=cm, vmin=0, vmax=vm,
extent=(0, Nx, 0, Ny), interpolation="nearest")
ax.imshow(np.abs(np.where(s1, np.nan, _vort(u1)))[1:-1, 1:-1], origin="lower", cmap=cm, vmin=0, vmax=vm,
extent=(ax1+0.5, bx1-0.5, ay1+0.5, by1-0.5), interpolation="nearest")
ax.add_patch(mpatches.Rectangle((0.2, 0.2), Nx-0.4, Ny-0.4, fill=False, edgecolor="#4fc3f7", lw=1.6))
ax.text(1.5, Ny-4, "L0: Δx", color="#4fc3f7", fontsize=9, weight="bold")
ax.add_patch(mpatches.Rectangle((ax1, ay1), bx1-ax1, by1-ay1, fill=False, edgecolor="#aed581", lw=2.2))
ax.text(ax1+1, by1-3.5, "L1: Δx/2 (тело+след)", color="#aed581", fontsize=9, weight="bold")
ax.set_title(f"2×+SDF · |\\omega| · t={t}", fontsize=11)
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]"); ax.set_xlim(0, Nx); ax.set_ylim(0, Ny)
buf = __import__("io").BytesIO(); fig.savefig(buf, format="png", bbox_inches="tight"); buf.seek(0); plt.close(fig)
pil.append(Image.open(buf).convert("RGB").convert("P", palette=Image.ADAPTIVE))
os.makedirs(os.path.dirname(path), exist_ok=True)
pil[0].save(path, save_all=True, append_images=pil[1:], duration=int(1000/fps), loop=0, optimize=True)
print("saved", os.path.basename(path), len(frames), "кадров")