feat: 水平卧式流化床空床流场 GPU LBM 仿真 (100 格/m)

- 3D D3Q19 LBM (numba.cuda), 240x130x265=8.27M 单元
- 恒功率风机曲线 dp*Q=P 压力进口, 工况点涌现
- 多孔布风板: 2mm 厚, 6.25% 开孔, 5 度倾斜(-x)
- 4 段 DN300 侧进风 / 567->1200 渐扩 / DN800 排风
- 输出 19 张切面/矢量/压差/流量/分区图 (4x 分辨率)
This commit is contained in:
2026-09-19 13:37:51 +08:00
commit 5b16b9e790
28 changed files with 1786 additions and 0 deletions
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
水平卧式流化床 空床流场 —— 批量出图
====================================
读取 fbed_fields.npz, 生成大量切面 / 分量 / 压力 / 分区图。
python tools/fbed_figures.py
"""
import os
import sys
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fbed_simulation as fs
OUT = fs.OUT
BG, FG, MUTED, ACCENT = fs.BG, fs.FG, fs.MUTED, fs.ACCENT
CMAP_FLOW = fs.CMAP_FLOW
XC, YC, ZC = fs.XC, fs.YC, fs.ZC
X0, X1, Y0, Y1, Z0, Z1 = fs.X0, fs.X1, fs.Y0, fs.Y1, fs.Z0, fs.Z1
PPM, DX, SCALE_U = fs.PPM, fs.DX, fs.SCALE_U
D = np.load(os.path.join(OUT, "fbed_fields.npz"))
UX, UY, UZ, RHO, SOLID = D["ux"], D["uy"], D["uz"], D["rho"], D["solid"]
SPEED = np.sqrt(UX ** 2 + UY ** 2 + UZ ** 2) * SCALE_U
P = (RHO - 1.0) / 3.0 * (1.2 * SCALE_U ** 2)
P[SOLID] = np.nan
CMAP_P = plt.get_cmap("RdBu_r")
def xi(v):
return fs._xi(v)
def yi(v):
return fs._yi(v)
def zi(v):
return fs._zi(v)
def get(which, idx):
if which == "xz":
return (SPEED[:, idx, :], UX[:, idx, :], UZ[:, idx, :], SOLID[:, idx, :],
[X0, X1, Z0, Z1], "x (m)", "z (m)")
if which == "yz":
return (SPEED[:, :, idx], UY[:, :, idx], UZ[:, :, idx], SOLID[:, :, idx],
[Y0, Y1, Z0, Z1], "y (m)", "z (m)")
return (SPEED[idx, :, :], UX[idx, :, :], UY[idx, :, :], SOLID[idx, :, :],
[X0, X1, Y0, Y1], "x (m)", "y (m)")
def get_scalar(field, which, idx):
if which == "xz":
return field[:, idx, :], SOLID[:, idx, :], [X0, X1, Z0, Z1], "x (m)", "z (m)"
if which == "yz":
return field[:, :, idx], SOLID[:, :, idx], [Y0, Y1, Z0, Z1], "y (m)", "z (m)"
return field[idx, :, :], SOLID[idx, :, :], [X0, X1, Y0, Y1], "x (m)", "y (m)"
def panel(ax, which, idx, vmax, stream=True, density=1.1):
s2, u2, v2, sol2, ext, xl, yl = get(which, idx)
ax.imshow(np.where(sol2, np.nan, s2), origin="lower", extent=ext, cmap=CMAP_FLOW,
vmin=0, vmax=vmax, interpolation="bilinear", aspect="equal")
if stream:
ny, nx = sol2.shape
xs = np.linspace(ext[0], ext[1], nx)
ys = np.linspace(ext[2], ext[3], ny)
U = np.where(sol2, np.nan, u2)
V = np.where(sol2, np.nan, v2)
with np.errstate(invalid="ignore"):
ax.streamplot(xs, ys, U, V, density=density,
color=(0.85, 0.88, 0.92, 0.38), linewidth=0.45,
arrowsize=0.5, zorder=5)
ax.contour(np.linspace(ext[0], ext[1], sol2.shape[1]),
np.linspace(ext[2], ext[3], sol2.shape[0]),
sol2.astype(float), levels=[0.5], colors="#adbac7", linewidths=0.7, zorder=6)
ax.set_xlabel(xl, fontsize=8)
ax.set_ylabel(yl, fontsize=8)
ax.set_xlim(ext[0], ext[1])
ax.set_ylim(ext[2], ext[3])
ax.tick_params(labelsize=7)
def panel_scalar(ax, field, which, idx, cmap, vmin=None, vmax=None, center=False, stream=True):
f2, sol2, ext, xl, yl = get_scalar(field, which, idx)
f2 = np.where(sol2, np.nan, f2)
if center:
lim = np.nanpercentile(np.abs(f2), 99) * 1.05
im = ax.imshow(f2, origin="lower", extent=ext, cmap=cmap, vmin=-lim, vmax=lim,
interpolation="bilinear", aspect="equal")
else:
im = ax.imshow(f2, origin="lower", extent=ext, cmap=cmap, vmin=vmin, vmax=vmax,
interpolation="bilinear", aspect="equal")
if stream:
s2, u2, v2, _, _, _, _ = get(which, idx)
ny, nx = sol2.shape
xs = np.linspace(ext[0], ext[1], nx)
ys = np.linspace(ext[2], ext[3], ny)
with np.errstate(invalid="ignore"):
ax.streamplot(xs, ys, np.where(sol2, np.nan, u2), np.where(sol2, np.nan, v2),
density=1.0, color=(0.85, 0.88, 0.92, 0.3), linewidth=0.4,
arrowsize=0.5, zorder=5)
ax.contour(np.linspace(ext[0], ext[1], sol2.shape[1]),
np.linspace(ext[2], ext[3], sol2.shape[0]),
sol2.astype(float), levels=[0.5], colors="#adbac7", linewidths=0.7, zorder=6)
ax.set_xlabel(xl, fontsize=8)
ax.set_ylabel(yl, fontsize=8)
ax.set_xlim(ext[0], ext[1])
ax.set_ylim(ext[2], ext[3])
ax.tick_params(labelsize=7)
return im
def montage(specs, ncols, suptitle, name, vmax, cbar_label="|U| (m/s)",
figw=17.0, panel_h=3.0, stream=True, density=1.1):
nrows = (len(specs) + ncols - 1) // ncols
fig, axes = plt.subplots(nrows, ncols, figsize=(figw, panel_h * nrows + 1.0),
squeeze=False)
for ax in axes.ravel()[len(specs):]:
ax.axis("off")
for ax, (spec, title) in zip(axes.ravel(), specs):
which, idx = spec
panel(ax, which, idx, vmax, stream=stream, density=density)
ax.set_title(title, fontsize=9.5, pad=4)
sm = plt.cm.ScalarMappable(cmap=CMAP_FLOW, norm=plt.Normalize(0, vmax))
cb = fig.colorbar(sm, ax=axes.ravel().tolist(), fraction=0.012, pad=0.01)
cb.set_label(cbar_label, fontsize=10)
cb.outline.set_edgecolor("#30363d")
fig.suptitle(suptitle, fontsize=15, y=0.995)
fig.savefig(os.path.join(OUT, name), dpi=500, bbox_inches="tight")
plt.close(fig)
print(" ->", name)
def fig_xy_plenum():
specs = [(("xy", zi(z)), f"z = {z:+.2f} m") for z in (-0.42, -0.32, -0.22, -0.12, -0.02)]
montage(specs, 3, "风箱层水平切面 |U|(4 段侧进风分布)", "fbed-montage-plenum.png", 6.0)
def fig_xy_plate():
specs = [(("xy", zi(z)), f"z = {z:.2f} m") for z in (0.04, 0.08, 0.13, 0.20, 0.30, 0.45)]
montage(specs, 3, "布风板近区水平切面 |U|(射流合并过程)", "fbed-montage-nearplate.png", 3.0)
def fig_xy_free():
specs = [(("xy", zi(z)), f"z = {z:.2f} m") for z in (0.60, 0.80, 1.00, 1.20, 1.45, 1.75)]
montage(specs, 3, "沸腾段 / 渐扩 / 排风 水平切面 |U|", "fbed-montage-freeboard.png", 4.0)
def fig_xz():
specs = [(("xz", yi(y)), f"y = {y:+.2f} m") for y in (-0.26, -0.13, 0.0, 0.13, 0.26)]
montage(specs, 3, "垂直切面 x-z(不同宽度位置)|U|", "fbed-montage-xz.png", 5.0, figw=16.0)
def fig_yz():
specs = [(("yz", xi(x)), f"x = {x:.2f} m") for x in (0.15, 0.45, 0.75, 1.05, 1.35, 1.65, 1.95, 2.25)]
montage(specs, 4, "垂直切面 y-z(沿长度)|U|", "fbed-montage-yz.png", 5.0, figw=18.0, panel_h=3.4)
def fig_components():
fig, axes = plt.subplots(2, 2, figsize=(15.0, 12.0))
panel(axes[0, 0], "xz", yi(0.0), 4.0)
axes[0, 0].set_title("(a) 速度大小 |U|", fontsize=12, color=ACCENT)
for ax, comp, title, cmap in [
(axes[0, 1], UX, "(b) 水平分量 ux(孔板 5° 倾斜 + 向排风汇流)", "coolwarm"),
(axes[1, 0], UY, "(c) 横向分量 uy(侧进风 + 渐扩)", "coolwarm"),
(axes[1, 1], UZ, "(d) 垂直分量 uz(上升主流)", "coolwarm")]:
f2, sol2, ext, xl, yl = get_scalar(comp * SCALE_U, "xz", yi(0.0))
lim = np.nanpercentile(np.abs(np.where(sol2, np.nan, f2)), 99) * 1.05
im = ax.imshow(np.where(sol2, np.nan, f2), origin="lower", extent=ext, cmap=cmap,
vmin=-lim, vmax=lim, interpolation="bilinear", aspect="equal")
ax.contour(np.linspace(ext[0], ext[1], sol2.shape[1]),
np.linspace(ext[2], ext[3], sol2.shape[0]),
sol2.astype(float), levels=[0.5], colors="#adbac7", linewidths=0.7)
ax.set_title(title, fontsize=12)
ax.set_xlabel("x (m)", fontsize=9)
ax.set_ylabel("z (m)", fontsize=9)
cb = fig.colorbar(im, ax=ax, fraction=0.036, pad=0.02)
cb.set_label("m/s", fontsize=9)
cb.outline.set_edgecolor("#30363d")
fig.suptitle("速度分量分解(垂直切面 y=0)", fontsize=15, y=0.99)
fig.tight_layout(rect=[0, 0, 1, 0.97])
fig.savefig(os.path.join(OUT, "fbed-components.png"), dpi=520)
plt.close(fig)
print(" -> fbed-components.png")
def fig_pressure():
fig, axes = plt.subplots(2, 2, figsize=(15.0, 12.0))
for ax, (which, idx, title) in zip(axes.ravel(), [
("xz", yi(0.0), "(a) 垂直切面 y=0 —— 全系统压差"),
("yz", xi(1.2), "(b) 垂直切面 x=1.2"),
("xy", zi(0.15), "(c) 布风板近区 z=0.15"),
("xy", zi(0.90), "(d) 沸腾段 z=0.90")]):
im = panel_scalar(ax, P, which, idx, CMAP_P, center=True, stream=False)
ax.set_title(title, fontsize=12)
cb = fig.colorbar(im, ax=ax, fraction=0.036, pad=0.02)
cb.set_label("静压 (Pa)", fontsize=9)
cb.outline.set_edgecolor("#30363d")
fig.suptitle("静压场(相对出口, 单位 Pa)", fontsize=15, y=0.99)
fig.tight_layout(rect=[0, 0, 1, 0.97])
fig.savefig(os.path.join(OUT, "fbed-pressure.png"), dpi=520)
plt.close(fig)
print(" -> fbed-pressure.png")
def fig_sections():
fig, axes = plt.subplots(1, 4, figsize=(17.0, 6.0))
for k, ax in enumerate(axes):
xc = 0.6 * (k + 0.5)
panel(ax, "yz", xi(xc), 5.0)
ax.set_title(f"第 {k+1} 段 x={xc:.1f} m", fontsize=11)
fig.suptitle("4 个进风区段中心垂直切面 y-z(单段风量 2500 m³/h)", fontsize=14, y=1.0)
fig.tight_layout(rect=[0, 0, 1, 0.95])
fig.savefig(os.path.join(OUT, "fbed-sections.png"), dpi=520)
plt.close(fig)
print(" -> fbed-sections.png")
def fig_hero_stream():
fig, axes = plt.subplots(1, 2, figsize=(18.0, 8.0))
panel(axes[0], "xz", yi(0.0), 4.0, density=1.8)
axes[0].set_title("垂直切面 y=0 —— 风箱/布风板/沸腾段/排风", fontsize=12)
panel(axes[1], "yz", xi(1.2), 4.0, density=1.8)
axes[1].set_title("垂直切面 x=1.2 —— 侧向渐扩", fontsize=12)
fig.suptitle("空床流场流线(GPU D3Q19 LBM)", fontsize=15, y=0.99)
fig.tight_layout(rect=[0, 0, 1, 0.96])
fig.savefig(os.path.join(OUT, "fbed-streamlines.png"), dpi=560)
plt.close(fig)
print(" -> fbed-streamlines.png")
def fig_3d():
from mpl_toolkits.mplot3d import Axes3D # noqa
fig = plt.figure(figsize=(13.0, 10.0))
ax = fig.add_subplot(111, projection="3d")
norm = plt.Normalize(0, 4.0)
def surf_y(iy):
f = SPEED[:, iy, :]
m = SOLID[:, iy, :]
X, Z = np.meshgrid(XC, ZC)
Y = np.full_like(X, YC[iy])
fc = CMAP_FLOW(norm(np.where(m, np.nan, f)))
fc[m, 3] = 0.0
ax.plot_surface(X, Y, Z, facecolors=fc, shade=False, rstride=1, cstride=1,
linewidth=0, antialiased=False)
def surf_x(ix):
f = SPEED[:, :, ix]
m = SOLID[:, :, ix]
Y, Z = np.meshgrid(YC, ZC)
X = np.full_like(Y, XC[ix])
fc = CMAP_FLOW(norm(np.where(m, np.nan, f)))
fc[m, 3] = 0.0
ax.plot_surface(X, Y, Z, facecolors=fc, shade=False, rstride=1, cstride=1,
linewidth=0, antialiased=False)
def surf_z(iz):
f = SPEED[iz, :, :]
m = SOLID[iz, :, :]
X, Y = np.meshgrid(XC, YC)
Z = np.full_like(X, ZC[iz])
fc = CMAP_FLOW(norm(np.where(m, np.nan, f)))
fc[m, 3] = 0.0
ax.plot_surface(X, Y, Z, facecolors=fc, shade=False, rstride=1, cstride=1,
linewidth=0, antialiased=False)
surf_y(yi(0.0))
surf_x(xi(1.2))
surf_z(zi(0.9))
ax.set_xlabel("x (m)")
ax.set_ylabel("y (m)")
ax.set_zlabel("z (m)")
ax.set_title("三正交切面三维剖切(|U|)", fontsize=13)
ax.view_init(elev=22, azim=-58)
sm = plt.cm.ScalarMappable(cmap=CMAP_FLOW, norm=norm)
cb = fig.colorbar(sm, ax=ax, fraction=0.03, pad=0.08, shrink=0.6)
cb.set_label("|U| (m/s)", fontsize=10)
cb.outline.set_edgecolor("#30363d")
fig.savefig(os.path.join(OUT, "fbed-3d-cutaway.png"), dpi=520)
plt.close(fig)
print(" -> fbed-3d-cutaway.png")
def main():
print("[figs] loading", os.path.join(OUT, "fbed_fields.npz"))
fig_xy_plenum()
fig_xy_plate()
fig_xy_free()
fig_xz()
fig_yz()
fig_components()
fig_pressure()
fig_sections()
fig_hero_stream()
fig_3d()
print("[figs] done")
if __name__ == "__main__":
main()