# src/tank_pipe/output.py """ Output helpers: persistence (.npz), static plots (.png/.html), animation (.gif). Uses matplotlib's Agg backend so it works in headless environments. The PillowWriter is used for GIF output to avoid an ffmpeg dependency. """ import html import os import numpy as np import matplotlib matplotlib.use("Agg") # headless-safe; must be set before pyplot import import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter def _pipe_primitives(history, gamma, R_gas): W_hist = history['W_hist'] rho = W_hist[:, 0, :] u = W_hist[:, 1, :] / rho P = (gamma - 1) * (W_hist[:, 2, :] - 0.5 * rho * u ** 2) T = P / (rho * R_gas) a = np.sqrt(gamma * P / rho) Ma = u / a return rho, u, P, T, a, Ma def save_history(history, pipe, path, gamma, R_gas): """ Persist the full simulation history + pipe geometry to a .npz file. Loadable later with: d = np.load("results/history.npz") rho = d['W_hist'][:, 0, :] u = d['W_hist'][:, 1, :] / rho P = (d['gamma'] - 1) * (d['W_hist'][:, 2, :] - 0.5 * rho * u**2) """ dirname = os.path.dirname(path) if dirname: os.makedirs(dirname, exist_ok=True) np.savez_compressed( path, t=history['t'], P1=history['P1'], T1=history['T1'], P2=history['P2'], T2=history['T2'], W_hist=history['W_hist'], x=pipe.x_centers, dx=pipe.dx, area=pipe.area, gamma=gamma, R_gas=R_gas, ) def plot_tank_pressure(history, path): fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(history['t'], history['P1'] / 1e6, label="Tank 1 (high pressure)") ax.plot(history['t'], history['P2'] / 1e6, label="Tank 2 (low pressure)") ax.set_xlabel("Time [s]") ax.set_ylabel("Pressure [MPa]") ax.set_title("Tank pressures vs time") ax.grid(True) ax.legend() fig.tight_layout() fig.savefig(path, dpi=120) plt.close(fig) def plot_tank_temperature(history, path): fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(history['t'], history['T1'], label="Tank 1 (high pressure)") ax.plot(history['t'], history['T2'], label="Tank 2 (low pressure)") ax.set_xlabel("Time [s]") ax.set_ylabel("Temperature [K]") ax.set_title("Tank temperatures vs time") ax.grid(True) ax.legend() fig.tight_layout() fig.savefig(path, dpi=120) plt.close(fig) def make_pipe_animation(history, pipe, path, gamma, R_gas, stride=10): """ Render a GIF of the pipe's P(x), u(x), T(x) evolution over time. Uses PillowWriter so no ffmpeg is needed. """ t = history['t'] x = pipe.x_centers n_steps = history['W_hist'].shape[0] # Frame indices: every `stride`th snapshot, plus the final one frames = list(range(0, n_steps, stride)) if frames[-1] != n_steps - 1: frames.append(n_steps - 1) # Precompute primitives for all frames in one vectorized pass _, u, P, T, _, Ma = _pipe_primitives(history, gamma, R_gas) fig, axes = plt.subplots(4, 1, figsize=(10, 12), sharex=True) # Pressure subplot line_P, = axes[0].plot(x, P[0] / 1e6) axes[0].set_ylabel("P [MPa]") axes[0].set_ylim(P.min() / 1e6 * 0.95, P.max() / 1e6 * 1.05) axes[0].grid(True) # Velocity subplot line_u, = axes[1].plot(x, u[0]) axes[1].set_ylabel("u [m/s]") u_min, u_max = float(u.min()), float(u.max()) pad = max(1.0, 0.05 * (u_max - u_min) if u_max > u_min else 1.0) axes[1].set_ylim(u_min - pad, u_max + pad) axes[1].grid(True) # Temperature subplot line_T, = axes[2].plot(x, T[0]) axes[2].set_ylabel("T [K]") axes[2].set_ylim(T.min() * 0.95, T.max() * 1.05) axes[2].grid(True) # Mach number subplot line_Ma, = axes[3].plot(x, Ma[0]) axes[3].set_ylabel("Mach [-]") axes[3].set_xlabel("x [m]") Ma_min, Ma_max = float(Ma.min()), float(Ma.max()) pad_Ma = max(0.05, 0.05 * (Ma_max - Ma_min) if Ma_max > Ma_min else 0.05) axes[3].set_ylim(Ma_min - pad_Ma, Ma_max + pad_Ma) axes[3].grid(True) title = fig.suptitle("") def update(frame_idx): line_P.set_ydata(P[frame_idx] / 1e6) line_u.set_ydata(u[frame_idx]) line_T.set_ydata(T[frame_idx]) line_Ma.set_ydata(Ma[frame_idx]) title.set_text( f"t = {t[frame_idx]:.5f} s (step {frame_idx + 1}/{n_steps})" ) return line_P, line_u, line_T, line_Ma, title anim = FuncAnimation(fig, update, frames=frames, interval=50, blit=False) dirname = os.path.dirname(path) if dirname: os.makedirs(dirname, exist_ok=True) anim.save(path, writer=PillowWriter(fps=20)) plt.close(fig) def plot_pipe_final_profiles(history, pipe, path, gamma, R_gas): """Plot final pipe P(x), u(x), T(x), and Mach(x) as a static PNG.""" x = pipe.x_centers _, u, P, T, _, Ma = _pipe_primitives(history, gamma, R_gas) fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharex=True) axes = axes.ravel() axes[0].plot(x, P[-1] / 1e6) axes[0].set_ylabel("P [MPa]") axes[0].set_title("Final pressure profile") axes[0].grid(True) axes[1].plot(x, u[-1]) axes[1].set_ylabel("u [m/s]") axes[1].set_title("Final velocity profile") axes[1].grid(True) axes[2].plot(x, T[-1]) axes[2].set_xlabel("x [m]") axes[2].set_ylabel("T [K]") axes[2].set_title("Final temperature profile") axes[2].grid(True) axes[3].plot(x, Ma[-1]) axes[3].axhline(1.0, color="r", linestyle="--", linewidth=1, label="Mach 1") axes[3].set_xlabel("x [m]") axes[3].set_ylabel("Mach [-]") axes[3].set_title("Final Mach profile") axes[3].grid(True) axes[3].legend() fig.tight_layout() fig.savefig(path, dpi=120) plt.close(fig) def write_summary_report(history, pipe, path, gamma, R_gas, config): """Write an HTML summary report for the latest simulation outputs.""" t = history['t'] P1 = history['P1'] T1 = history['T1'] P2 = history['P2'] T2 = history['T2'] W_hist = history['W_hist'] x = pipe.x_centers _, u, P, T, _, Ma = _pipe_primitives(history, gamma, R_gas) V1 = config['V1'] V2 = config['V2'] m1_init = P1[0] * V1 / (R_gas * T1[0]) m1_final = P1[-1] * V1 / (R_gas * T1[-1]) m2_init = P2[0] * V2 / (R_gas * T2[0]) m2_final = P2[-1] * V2 / (R_gas * T2[-1]) mpipe_init = float(np.sum(W_hist[0, 0, :] * pipe.area * pipe.dx)) mpipe_final = float(np.sum(W_hist[-1, 0, :] * pipe.area * pipe.dx)) m_total_init = m1_init + m2_init + mpipe_init m_total_final = m1_final + m2_final + mpipe_final rel_err_m = abs(m_total_final - m_total_init) / m_total_init U1_init = P1[0] * V1 / (gamma - 1) U1_final = P1[-1] * V1 / (gamma - 1) U2_init = P2[0] * V2 / (gamma - 1) U2_final = P2[-1] * V2 / (gamma - 1) Upipe_init = float(np.sum(W_hist[0, 2, :] * pipe.area * pipe.dx)) Upipe_final = float(np.sum(W_hist[-1, 2, :] * pipe.area * pipe.dx)) U_total_init = U1_init + U2_init + Upipe_init U_total_final = U1_final + U2_final + Upipe_final rel_err_U = abs(U_total_final - U_total_init) / U_total_init idx_ma = np.unravel_index(np.argmax(Ma), Ma.shape) idx_p = np.unravel_index(np.argmax(P), P.shape) sections = [ ("Simulation setup", [ ("Gamma", f"{gamma:.3f}"), ("R_gas", f"{R_gas:.3f} J/(kg K)"), ("Tank 1", f"V={V1:.3f} m^3, P0={config['P1_INIT']/1e6:.6f} MPa, T0={config['T1_INIT']:.3f} K"), ("Tank 2", f"V={V2:.3f} m^3, P0={config['P2_INIT']/1e6:.6f} MPa, T0={config['T2_INIT']:.3f} K"), ("Pipe", f"L={config['L']:.3f} m, D={config['D']*1e3:.3f} mm, N={config['N_CELLS']}"), ("Run control", f"t_end={config['T_END']:.6f} s, CFL={config['CFL']:.3f}, steps={len(t)}"), ]), ("Tank states", [ ("Tank 1 pressure", f"{P1[0]/1e6:.6f} -> {P1[-1]/1e6:.6f} MPa"), ("Tank 2 pressure", f"{P2[0]/1e6:.6f} -> {P2[-1]/1e6:.6f} MPa"), ("Tank 1 temperature", f"{T1[0]:.6f} -> {T1[-1]:.6f} K"), ("Tank 2 temperature", f"{T2[0]:.6f} -> {T2[-1]:.6f} K"), ]), ("Pipe extrema", [ ("Max pressure", f"{P[idx_p]/1e6:.6f} MPa at t={t[idx_p[0]]:.6e} s, x={x[idx_p[1]]:.6f} m"), ("Max velocity", f"{u.max():.6f} m/s"), ("Min / max temperature", f"{T.min():.6f} / {T.max():.6f} K"), ("Max Mach", f"{Ma[idx_ma]:.6f} at t={t[idx_ma[0]]:.6e} s, x={x[idx_ma[1]]:.6f} m"), ("Final Mach range", f"{Ma[-1].min():.6f} -> {Ma[-1].max():.6f}"), ("Final supersonic cells", f"{int(np.sum(Ma[-1] > 1.0))} / {Ma.shape[1]}"), ]), ("Conservation check", [ ("Total mass", f"{m_total_init:.12e} -> {m_total_final:.12e} kg (rel err {rel_err_m:.3e})"), ("Total energy", f"{U_total_init:.12e} -> {U_total_final:.12e} J (rel err {rel_err_U:.3e})"), ]), ] parts = [ "", "", "
", "", "Generated from results/history.npz. Final simulation time: {t[-1]:.6f} s.
| {html.escape(str(key))} | {html.escape(str(value))} |
|---|



Animation: pipe_animation.gif
", "", "", ]) dirname = os.path.dirname(path) if dirname: os.makedirs(dirname, exist_ok=True) with open(path, "w", encoding="utf-8") as f: f.write("\n".join(parts))