#!/usr/bin/env python # -*- coding: utf-8 -*- """ 水平卧式流化床 空床流场 —— 分析图 (几何/矢量/压差/流量/均匀性/衰减/倾斜/分段放大) 读取 fbed_fields.npz。 python tools/fbed_analysis.py """ import os import sys import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import Rectangle 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 DX, SCALE_U, BED_L, BED_W, N_SEC, SEC_L = fs.DX, fs.SCALE_U, fs.BED_L, fs.BED_W, fs.N_SEC, fs.SEC_L PPM = fs.PPM NZ, NY, NX = fs.NZ, fs.NY, fs.NX 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"] PLATE = D["plate"] FLUID = ~SOLID UX, UY, UZ = UX * SCALE_U, UY * SCALE_U, UZ * SCALE_U SPEED = np.sqrt(UX ** 2 + UY ** 2 + UZ ** 2) P = (RHO - 1.0) / 3.0 * (1.2 * SCALE_U ** 2) CELL_A = DX * DX IZP = fs._zi(0.0) def zi(v): return fs._zi(v) def yi(v): return fs._yi(v) def xi(v): return fs._xi(v) def quiver_slice(ax, which, idx, step=3, scale=None, title="", vmax=None): if which == "xz": ext = [X0, X1, Z0, Z1]; xl, yl = "x (m)", "z (m)" U, V, M = UX[:, idx, :], UZ[:, idx, :], SPEED[:, idx, :] m = SOLID[:, idx, :] xs, ys = XC, ZC elif which == "yz": ext = [Y0, Y1, Z0, Z1]; xl, yl = "y (m)", "z (m)" U, V, M = UY[:, :, idx], UZ[:, :, idx], SPEED[:, :, idx] m = SOLID[:, :, idx] xs, ys = YC, ZC else: ext = [X0, X1, Y0, Y1]; xl, yl = "x (m)", "y (m)" U, V, M = UX[idx, :, :], UY[idx, :, :], SPEED[idx, :, :] m = SOLID[idx, :, :] xs, ys = XC, YC U = np.where(m, np.nan, U); V = np.where(m, np.nan, V); M = np.where(m, np.nan, M) ax.imshow(np.where(m, np.nan, M), origin="lower", extent=ext, cmap=CMAP_FLOW, vmin=0, vmax=vmax or np.nanpercentile(M, 99), interpolation="bilinear", aspect="equal", alpha=0.85) Xs, Ys = np.meshgrid(xs[::step], ys[::step]) Us = U[::step, ::step]; Vs = V[::step, ::step]; Ms = M[::step, ::step] if scale is None: scale = 6.0 * max(ext[1] - ext[0], ext[3] - ext[2]) / max(np.nanmax(np.hypot(Us, Vs)), 1e-6) ax.quiver(Xs, Ys, Us, Vs, Ms, cmap="cool", scale=scale, width=0.0022, clim=(0, np.nanpercentile(M, 99))) ax.contour(np.linspace(ext[0], ext[1], m.shape[1]), np.linspace(ext[2], ext[3], m.shape[0]), m.astype(float), levels=[0.5], colors="#adbac7", linewidths=0.8) ax.set_xlim(ext[0], ext[1]); ax.set_ylim(ext[2], ext[3]) ax.set_xlabel(xl, fontsize=9); ax.set_ylabel(yl, fontsize=9) ax.tick_params(labelsize=8) if title: ax.set_title(title, fontsize=12) # -------------------------------------------------------------------------- def fig_geometry(): fig, axes = plt.subplots(1, 2, figsize=(17.0, 7.6)) for ax, which, idx in [(axes[0], "xz", yi(0.0)), (axes[1], "yz", xi(1.2))]: if which == "xz": ext = [X0, X1, Z0, Z1]; sol = SOLID[:, idx, :] xl, yl = "x (m)", "z (m)" else: ext = [Y0, Y1, Z0, Z1]; sol = SOLID[:, :, idx] xl, yl = "y (m)", "z (m)" ax.imshow(np.where(sol, 0.55, 0.08), origin="lower", extent=ext, cmap="gray", vmin=0, vmax=1, interpolation="nearest", aspect="equal") ax.contour(np.linspace(ext[0], ext[1], sol.shape[1]), np.linspace(ext[2], ext[3], sol.shape[0]), sol.astype(float), levels=[0.5], colors="#adbac7", linewidths=1.0) ax.set_xlabel(xl, fontsize=10); ax.set_ylabel(yl, fontsize=10) # 标注 (在 x-z 图) ax = axes[0] ax.annotate("4 x DN300 侧进风\n(平行床板, 2500 m³/h/段)", xy=(0.05, -0.225), xytext=(-0.35, 0.9), fontsize=10, color="#7ee787", arrowprops=dict(arrowstyle="->", color="#7ee787", lw=1.6)) ax.annotate("布风板 2mm孔 5.6% 5°倾斜", xy=(1.2, 0.0), xytext=(1.55, -0.35), fontsize=10, color="#58a6ff", arrowprops=dict(arrowstyle="->", color="#58a6ff", lw=1.4)) ax.annotate("顶部仅 DN800 排风\n(无进风)", xy=(1.2, 2.1), xytext=(1.45, 1.55), fontsize=10.5, color="#ff7b72", arrowprops=dict(arrowstyle="->", color="#ff7b72", lw=1.6)) ax.set_title("几何 / 边界示意 (x-z, y=0)", fontsize=12) axes[1].set_title("几何 / 边界示意 (y-z, x=1.2)", fontsize=12) fig.suptitle("空床几何与边界条件:底部 4 段侧进风,顶部仅排风", fontsize=15, y=0.98) fig.tight_layout(rect=[0, 0, 1, 0.95]) fig.savefig(os.path.join(OUT, "fbed-geometry.png"), dpi=540) plt.close(fig) print(" -> fbed-geometry.png") def fig_vectors(): fig, axes = plt.subplots(2, 2, figsize=(16.0, 12.5)) quiver_slice(axes[0, 0], "xz", yi(0.0), step=3, title="(a) 垂直切面 y=0 速度矢量") quiver_slice(axes[0, 1], "yz", xi(1.2), step=3, title="(b) 垂直切面 x=1.2 速度矢量") quiver_slice(axes[1, 0], "xy", zi(0.15), step=3, title="(c) 布风板近区 z=0.15 速度矢量") quiver_slice(axes[1, 1], "xy", zi(0.90), step=3, title="(d) 沸腾段 z=0.90 速度矢量") fig.suptitle("空床流场速度矢量图(箭头方向=流向, 颜色=速度大小 m/s)", fontsize=15, y=0.99) fig.tight_layout(rect=[0, 0, 1, 0.97]) fig.savefig(os.path.join(OUT, "fbed-vectors.png"), dpi=520) plt.close(fig) print(" -> fbed-vectors.png") def fig_tilt(): fig = plt.figure(figsize=(16.0, 10.5)) gs = fig.add_gridspec(2, 2) ax = fig.add_subplot(gs[0, 0]) iz = zi(0.05) quiver_slice(ax, "xy", iz, step=4, vmax=2.5, title="(a) 布风板正上方 z=0.05 矢量(放大看 +x 倾斜)") ax.set_xlim(0.6, 1.8); ax.set_ylim(-0.30, 0.30) ax = fig.add_subplot(gs[0, 1]) zs, ang, um = [], [], [] for z in np.arange(0.03, 0.60, 0.02): iz = zi(z) m = FLUID[iz] & (np.abs(YC)[:, None] < BED_W / 2) if m.sum() < 10: continue ux_m, uz_m = UX[iz][m].mean(), UZ[iz][m].mean() zs.append(z); ang.append(np.degrees(np.arctan2(ux_m, uz_m))); um.append(ux_m) ax.plot(zs, ang, "-o", color=ACCENT, ms=4, label="实际偏角") ax.axhline(5.0, color="#ff7b72", ls="--", lw=1.2, label="理论 +5°") ax.axhline(0.0, color=MUTED, lw=0.8) ax.set_xlabel("高度 z (m)", fontsize=10); ax.set_ylabel("流向偏角 (°)", fontsize=10) ax.set_title("(b) 近板流向偏角随高度衰减", fontsize=12) ax.legend(fontsize=9); ax.grid(alpha=0.2) ax = fig.add_subplot(gs[1, 0]) zs2, uxs, uzs = [], [], [] for z in np.arange(0.03, 0.60, 0.02): iz = zi(z) m = FLUID[iz] & (np.abs(YC)[:, None] < BED_W / 2) if m.sum() < 10: continue zs2.append(z); uxs.append(UX[iz][m].mean()); uzs.append(UZ[iz][m].mean()) ax.plot(zs2, uzs, "-o", ms=4, color="#39d0d8", label="垂直分量 ") ax.plot(zs2, uxs, "-s", ms=4, color="#ff7b72", label="水平分量 (5° 驱动)") ax.set_xlabel("高度 z (m)", fontsize=10); ax.set_ylabel("面平均速度 (m/s)", fontsize=10) ax.set_title("(c) 近板面平均速度分量", fontsize=12) ax.legend(fontsize=9); ax.grid(alpha=0.2) ax = fig.add_subplot(gs[1, 1]) xline = np.linspace(0.02, BED_L - 0.02, 60) iz = zi(0.06) prof = [] for xv in xline: i = xi(xv) m = FLUID[iz, :, i] prof.append(UX[iz, :, i][m].mean() if m.sum() else np.nan) ax.plot(xline, prof, color="#ff7b72", lw=1.6) ax.axhline(0, color=MUTED, lw=0.8) ax.set_xlabel("x (m)", fontsize=10); ax.set_ylabel(" (m/s)", fontsize=10) ax.set_title("(d) 近板水平速度沿长度分布(整体 +x 偏置)", fontsize=12) ax.grid(alpha=0.2) fig.suptitle("布风板 5° 倾斜对近板风向的影响", fontsize=15, y=0.99) fig.tight_layout(rect=[0, 0, 1, 0.97]) fig.savefig(os.path.join(OUT, "fbed-tilt.png"), dpi=520) plt.close(fig) print(" -> fbed-tilt.png") def fig_uniformity(): fig, axes = plt.subplots(1, 3, figsize=(17.0, 5.4)) qs = [] for k in range(N_SEC): ix0, ix1 = xi(0.6 * k), xi(0.6 * (k + 1)) q = UZ[IZP, :, ix0:ix1][FLUID[IZP, :, ix0:ix1]].sum() * CELL_A * 3600.0 qs.append(q) ax = axes[0] bars = ax.bar([f"第{k+1}段" for k in range(N_SEC)], qs, color=ACCENT, alpha=0.85) ax.axhline(2500, color="#ff7b72", ls="--", lw=1.4, label="目标 2500 m³/h") for b, q in zip(bars, qs): ax.text(b.get_x() + b.get_width() / 2, q + 40, f"{q:.0f}", ha="center", fontsize=9) ax.set_ylabel("布风板风量 (m³/h)", fontsize=10); ax.legend(fontsize=9) ax.set_title("(a) 各区段风量", fontsize=12); ax.grid(alpha=0.2, axis="y") ax = axes[1] prof = [] xline = np.linspace(0.02, BED_L - 0.02, 120) for xv in xline: i = xi(xv) m = FLUID[IZP + 3, :, i] prof.append(UZ[IZP + 3, :, i][m].mean() if m.sum() else np.nan) ax.plot(xline, prof, color="#39d0d8", lw=1.8) ax.axhline(2500.0 / (BED_L * BED_W) / 3600.0 * 3600.0, color=MUTED, ls=":", lw=1) for k in range(1, N_SEC): ax.axvline(0.6 * k, color="#30363d", lw=0.8) ax.set_xlabel("x (m)", fontsize=10); ax.set_ylabel("近板 (m/s)", fontsize=10) ax.set_title("(b) 近板上升速度沿长度分布(均匀性)", fontsize=12); ax.grid(alpha=0.2) ax = axes[2] yline = np.linspace(-BED_W / 2 + 0.02, BED_W / 2 - 0.02, 60) profy = [] for yv in yline: j = yi(yv) m = FLUID[IZP + 3, j, :] profy.append(UZ[IZP + 3, j, :][m].mean() if m.sum() else np.nan) ax.plot(yline, profy, color="#7ee787", lw=1.8) ax.set_xlabel("y (m)", fontsize=10); ax.set_ylabel("近板 (m/s)", fontsize=10) ax.set_title("(c) 近板上升速度沿宽度分布(均匀性)", fontsize=12); ax.grid(alpha=0.2) fig.suptitle("布风板流量均匀性", fontsize=15, y=1.0) fig.tight_layout(rect=[0, 0, 1, 0.95]) fig.savefig(os.path.join(OUT, "fbed-uniformity.png"), dpi=520) plt.close(fig) print(" -> fbed-uniformity.png") def fig_decay(): fig, axes = plt.subplots(1, 2, figsize=(15.0, 7.0)) zs, qz, uz_avg, spd_avg, area = [], [], [], [], [] for iz in range(NZ): m = FLUID[iz] if m.sum() < 20: continue zs.append(ZC[iz]) qz.append(UZ[iz][m].sum() * CELL_A * 3600.0) uz_avg.append(UZ[iz][m].mean()) spd_avg.append(SPEED[iz][m].mean()) area.append(m.sum() * CELL_A) ax = axes[0] ax.plot(uz_avg, zs, "-o", ms=3, color="#39d0d8", label="面平均 ") ax.plot(spd_avg, zs, "-s", ms=3, color="#ff7b72", label="面平均 |U|") ax.axvline(0, color=MUTED, lw=0.8) ax.axhline(0.0, color="#30363d", lw=0.8) ax.set_xlabel("速度 (m/s)", fontsize=10); ax.set_ylabel("高度 z (m)", fontsize=10) ax.set_title("(a) 沿高度气速衰减", fontsize=12); ax.legend(fontsize=9); ax.grid(alpha=0.2) ax = axes[1] ax.plot(qz, zs, "-o", ms=3, color=ACCENT, label="通过水平面的净风量") ax.axvline(10000, color="#ff7b72", ls="--", lw=1.4, label="总风量 10000 m³/h") ax.set_xlabel("风量 (m³/h)", fontsize=10); ax.set_ylabel("高度 z (m)", fontsize=10) ax.set_title("(b) 沿高度风量守恒(连续性)", fontsize=12) ax.legend(fontsize=9); ax.grid(alpha=0.2) fig.suptitle("沿高度气速衰减与风量守恒", fontsize=15, y=0.99) fig.tight_layout(rect=[0, 0, 1, 0.96]) fig.savefig(os.path.join(OUT, "fbed-decay.png"), dpi=520) plt.close(fig) print(" -> fbed-decay.png") def fig_zoom_sections(): fig, axes = plt.subplots(4, 1, figsize=(11.0, 16.0)) for k, ax in enumerate(axes): quiver_slice(ax, "xz", yi(0.0), step=2, title=f"第 {k+1} 段 x∈[{0.6*k:.1f}, {0.6*(k+1):.1f}] m") ax.set_xlim(0.6 * k - 0.02, 0.6 * (k + 1) + 0.02) fig.suptitle("按区段剖分的 x-z 局部放大(含风箱进风/布风板/沸腾段)", fontsize=15, y=0.995) fig.tight_layout(rect=[0, 0, 1, 0.975]) fig.savefig(os.path.join(OUT, "fbed-zoom-sections.png"), dpi=500) plt.close(fig) print(" -> fbed-zoom-sections.png") def fig_pressure_drop(): fig = plt.figure(figsize=(16.0, 10.5)) gs = fig.add_gridspec(2, 2) ax = fig.add_subplot(gs[0, 0]) ext = [X0, X1, Z0, Z1] Pxz = np.where(SOLID[:, yi(0), :], np.nan, P[:, yi(0), :]) lim = np.nanpercentile(np.abs(Pxz), 99) im = ax.imshow(Pxz, origin="lower", extent=ext, cmap="RdBu_r", vmin=-lim, vmax=lim, aspect="equal", interpolation="bilinear") ax.axhline(0, color="#7ee787", lw=1.0, ls="--") ax.set_title("(a) 静压场 x-z (y=0),绿线=布风板", fontsize=12) ax.set_xlabel("x (m)"); ax.set_ylabel("z (m)") cb = fig.colorbar(im, ax=ax, fraction=0.036, pad=0.02); cb.set_label("Pa", fontsize=9) ax = fig.add_subplot(gs[0, 1]) ext = [X0, X1, Y0, Y1] Pxy = np.where(SOLID[zi(0.10)], np.nan, P[zi(0.10)]) im = ax.imshow(Pxy, origin="lower", extent=ext, cmap="RdBu_r", vmin=-np.nanpercentile(np.abs(Pxy), 99), vmax=np.nanpercentile(np.abs(Pxy), 99), aspect="equal", interpolation="bilinear") ax.set_title("(b) 布风板正上方 z=0.10 静压", fontsize=12) ax.set_xlabel("x (m)"); ax.set_ylabel("y (m)") cb = fig.colorbar(im, ax=ax, fraction=0.036, pad=0.02); cb.set_label("Pa", fontsize=9) ax = fig.add_subplot(gs[1, 0]) dps = [] for k in range(N_SEC): ix0, ix1 = xi(0.6 * k), xi(0.6 * (k + 1)) mp = FLUID[zi(-0.30):zi(-0.05), :, ix0:ix1] pp = P[zi(-0.30):zi(-0.05), :, ix0:ix1][mp].mean() mf = FLUID[zi(0.05):zi(0.20), :, ix0:ix1] pf = P[zi(0.05):zi(0.20), :, ix0:ix1][mf].mean() dps.append(pp - pf) bars = ax.bar([f"第{k+1}段" for k in range(N_SEC)], dps, color="#ff7b72", alpha=0.85) ax.axhline(2900, color="#7ee787", ls="--", lw=1.4, label="设计压差 2900 Pa") for b, v in zip(bars, dps): ax.text(b.get_x() + b.get_width() / 2, v + 5, f"{v:.0f}", ha="center", fontsize=9) ax.set_ylabel("布风板压差 (Pa)", fontsize=10); ax.legend(fontsize=9) ax.set_title("(c) 各段布风板压差(模型值)", fontsize=12); ax.grid(alpha=0.2, axis="y") ax = fig.add_subplot(gs[1, 1]) ext = [X0, X1, Z0, Z1] Pxz2 = np.where(SOLID[:, yi(0), :], np.nan, P[:, yi(0), :]) ax.plot(XC, np.nanmean(P[:, yi(0), :], axis=0), color="#39d0d8", lw=1.4, label="沿 x 平均 (y=0)") for zc, col in [(zi(-0.30), "#58a6ff"), (zi(-0.05), "#7ee787"), (zi(0.10), "#ff7b72")]: ax.plot(XC, np.where(SOLID[zc, yi(0), :], np.nan, P[zc, yi(0), :]), lw=1.0, color=col, label=f"z={ZC[zc]:.2f} m") ax.set_xlabel("x (m)", fontsize=10); ax.set_ylabel("静压 (Pa)", fontsize=10) ax.set_title("(d) 静压沿长度剖面", fontsize=12); ax.legend(fontsize=8); ax.grid(alpha=0.2) fig.suptitle("压差分析:布风板上下压差与静压分布", fontsize=15, y=0.99) fig.tight_layout(rect=[0, 0, 1, 0.97]) fig.savefig(os.path.join(OUT, "fbed-pressure-drop.png"), dpi=520) plt.close(fig) print(" -> fbed-pressure-drop.png") def fig_flowrate(): fig, axes = plt.subplots(1, 3, figsize=(17.0, 5.4)) qs = [] for k in range(N_SEC): ix0, ix1 = xi(0.6 * k), xi(0.6 * (k + 1)) q = UZ[IZP, :, ix0:ix1][FLUID[IZP, :, ix0:ix1]].sum() * CELL_A * 3600.0 qs.append(q) ax = axes[0] ax.bar([f"第{k+1}段" for k in range(N_SEC)], qs, color="#39d0d8", alpha=0.85) ax.axhline(2500, color="#ff7b72", ls="--", lw=1.4, label="目标 2500") ax.set_ylabel("风量 (m³/h)", fontsize=10); ax.legend(fontsize=9) ax.set_title("(a) 各区段布风板风量", fontsize=12); ax.grid(alpha=0.2, axis="y") ax = axes[1] cum = np.cumsum([UZ[IZP, :, i][FLUID[IZP, :, i]].sum() for i in range(NX)]) * CELL_A * 3600.0 ax.plot(XC, cum, color=ACCENT, lw=1.8) ax.axhline(10000, color="#ff7b72", ls="--", lw=1.2, label="总计 10000") ax.set_xlabel("x (m)", fontsize=10); ax.set_ylabel("累计风量 (m³/h)", fontsize=10) ax.set_title("(b) 沿长度累计风量", fontsize=12); ax.legend(fontsize=9); ax.grid(alpha=0.2) ax = axes[2] iz_in = yi(fs.PIPE_Y0) qin = UY[:, iz_in, :][FLUID[:, iz_in, :]].sum() * CELL_A * 3600.0 iz_out = NZ - 2 qout = UZ[iz_out][FLUID[iz_out]].sum() * CELL_A * 3600.0 qplate = sum(qs) labels = ["进风\n(DN300×4)", "布风板\n(通过)", "排风\n(DN800)"] vals = [qin, qplate, qout] cols = ["#7ee787", "#39d0d8", "#ff7b72"] b = ax.bar(labels, vals, color=cols, alpha=0.85) for bb, v in zip(b, vals): ax.text(bb.get_x() + bb.get_width() / 2, v + 60, f"{v:.0f}", ha="center", fontsize=9.5) ax.axhline(10000, color=MUTED, ls=":", lw=1) ax.set_ylabel("风量 (m³/h)", fontsize=10) ax.set_title("(c) 进风 / 布风板 / 排风 风量平衡", fontsize=12); ax.grid(alpha=0.2, axis="y") fig.suptitle("流量分析", fontsize=15, y=1.0) fig.tight_layout(rect=[0, 0, 1, 0.95]) fig.savefig(os.path.join(OUT, "fbed-flowrate.png"), dpi=520) plt.close(fig) print(" -> fbed-flowrate.png") def main(): fig_geometry() fig_vectors() fig_tilt() fig_uniformity() fig_decay() fig_zoom_sections() fig_pressure_drop() fig_flowrate() print("[analysis] done") if __name__ == "__main__": main()