|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Generate GIF animation from Sod Shock Tube VTK files. |
| 4 | +
|
| 5 | +Works with both SPH and GSPH solver outputs. |
| 6 | +Uses shamrock.phys.SodTube for analytical solution (no custom Python implementation). |
| 7 | +
|
| 8 | +Usage: |
| 9 | + python animate_sod_vtk.py <vtk_dir> [output_dir] [--solver SPH|GSPH] |
| 10 | +
|
| 11 | +Examples: |
| 12 | + python animate_sod_vtk.py simulations_data/gsph_sod/vtk --solver GSPH |
| 13 | + python animate_sod_vtk.py simulations_data/sph_sod/vtk --solver SPH |
| 14 | +""" |
| 15 | + |
| 16 | +import argparse |
| 17 | +import glob |
| 18 | +import os |
| 19 | +import sys |
| 20 | + |
| 21 | +import matplotlib |
| 22 | + |
| 23 | +matplotlib.use("Agg") |
| 24 | +import matplotlib.pyplot as plt |
| 25 | +import numpy as np |
| 26 | +import pyvista as pv |
| 27 | +from matplotlib.animation import FuncAnimation, PillowWriter |
| 28 | + |
| 29 | +# Import shamrock for analytical solution |
| 30 | +try: |
| 31 | + import shamrock |
| 32 | + |
| 33 | + HAS_SHAMROCK = True |
| 34 | +except ImportError: |
| 35 | + HAS_SHAMROCK = False |
| 36 | + print("Warning: shamrock module not found. Analytical solution will not be shown.") |
| 37 | + |
| 38 | + |
| 39 | +def parse_args(): |
| 40 | + parser = argparse.ArgumentParser(description="Animate Sod shock tube VTK results") |
| 41 | + parser.add_argument("vtk_dir", help="Directory containing VTK files") |
| 42 | + parser.add_argument( |
| 43 | + "output_dir", |
| 44 | + nargs="?", |
| 45 | + default=None, |
| 46 | + help="Output directory (defaults to parent of vtk_dir)", |
| 47 | + ) |
| 48 | + parser.add_argument( |
| 49 | + "--solver", |
| 50 | + choices=["SPH", "GSPH"], |
| 51 | + default="GSPH", |
| 52 | + help="Solver type (affects file naming)", |
| 53 | + ) |
| 54 | + parser.add_argument("--gamma", type=float, default=1.4, help="Adiabatic index (default: 1.4)") |
| 55 | + parser.add_argument( |
| 56 | + "--t-final", |
| 57 | + type=float, |
| 58 | + default=0.245, |
| 59 | + help="Final simulation time (default: 0.245)", |
| 60 | + ) |
| 61 | + parser.add_argument( |
| 62 | + "--fps", type=int, default=10, help="Animation frames per second (default: 10)" |
| 63 | + ) |
| 64 | + return parser.parse_args() |
| 65 | + |
| 66 | + |
| 67 | +def get_analytical_solution(sod, t, x_array): |
| 68 | + """Get analytical solution at multiple x positions using shamrock.phys.SodTube.""" |
| 69 | + rho = np.zeros(len(x_array)) |
| 70 | + vel = np.zeros(len(x_array)) |
| 71 | + pres = np.zeros(len(x_array)) |
| 72 | + for i, x in enumerate(x_array): |
| 73 | + rho[i], vel[i], pres[i] = sod.get_value(t, x) |
| 74 | + return x_array, rho, vel, pres |
| 75 | + |
| 76 | + |
| 77 | +def read_vtk(filename): |
| 78 | + """Read VTK file using pyvista.""" |
| 79 | + mesh = pv.read(filename) |
| 80 | + points = np.array(mesh.points) |
| 81 | + velocities = np.array(mesh["v"]) |
| 82 | + hpart = np.array(mesh["h"]) |
| 83 | + rho = np.array(mesh["rho"]) |
| 84 | + P = np.array(mesh["P"]) |
| 85 | + return points, velocities, hpart, rho, P |
| 86 | + |
| 87 | + |
| 88 | +def main(): |
| 89 | + args = parse_args() |
| 90 | + |
| 91 | + vtk_dir = args.vtk_dir |
| 92 | + output_dir = args.output_dir or os.path.dirname(vtk_dir) |
| 93 | + solver_name = args.solver |
| 94 | + gamma = args.gamma |
| 95 | + t_final = args.t_final |
| 96 | + |
| 97 | + # Find VTK files |
| 98 | + vtk_pattern = os.path.join(vtk_dir, "*.vtk") |
| 99 | + vtk_files = sorted(glob.glob(vtk_pattern)) |
| 100 | + |
| 101 | + print(f"{'=' * 70}") |
| 102 | + print(f"Sod Shock Tube Animation ({solver_name})") |
| 103 | + print(f"{'=' * 70}") |
| 104 | + print(f"VTK directory: {vtk_dir}") |
| 105 | + print(f"Output directory: {output_dir}") |
| 106 | + print(f"Found {len(vtk_files)} VTK files") |
| 107 | + print() |
| 108 | + |
| 109 | + if len(vtk_files) == 0: |
| 110 | + print(f"ERROR: No VTK files found in {vtk_dir}") |
| 111 | + sys.exit(1) |
| 112 | + |
| 113 | + n_frames = len(vtk_files) |
| 114 | + dt_dump = t_final / n_frames |
| 115 | + |
| 116 | + # Create analytical solver using shamrock.phys.SodTube |
| 117 | + sod_solver = None |
| 118 | + if HAS_SHAMROCK: |
| 119 | + # Standard Sod problem: left state (rho=1, P=1), right state (rho=0.125, P=0.1) |
| 120 | + sod_solver = shamrock.phys.SodTube( |
| 121 | + gamma=gamma, |
| 122 | + rho_1=1.0, # Left density |
| 123 | + P_1=1.0, # Left pressure |
| 124 | + rho_5=0.125, # Right density |
| 125 | + P_5=0.1, # Right pressure |
| 126 | + ) |
| 127 | + print(f"Analytical solution: shamrock.phys.SodTube (gamma={gamma})") |
| 128 | + else: |
| 129 | + print("Analytical solution: not available") |
| 130 | + print() |
| 131 | + |
| 132 | + # Set up figure |
| 133 | + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) |
| 134 | + |
| 135 | + def update(frame): |
| 136 | + vtk_file = vtk_files[frame] |
| 137 | + t = frame * dt_dump |
| 138 | + |
| 139 | + # Read data |
| 140 | + points, velocities, h, rho, P = read_vtk(vtk_file) |
| 141 | + |
| 142 | + x = points[:, 0] |
| 143 | + vx = velocities[:, 0] |
| 144 | + |
| 145 | + # Sort by x |
| 146 | + idx = np.argsort(x) |
| 147 | + x_sort = x[idx] |
| 148 | + rho_sort = rho[idx] |
| 149 | + vx_sort = vx[idx] |
| 150 | + P_sort = P[idx] |
| 151 | + h_sort = h[idx] |
| 152 | + |
| 153 | + # Clear and redraw |
| 154 | + for ax in axes.flat: |
| 155 | + ax.clear() |
| 156 | + |
| 157 | + # Plot analytical solution if available |
| 158 | + if sod_solver is not None and t > 0: |
| 159 | + x_ana = np.linspace(-1.0, 1.0, 500) |
| 160 | + _, rho_ana, vx_ana, P_ana = get_analytical_solution(sod_solver, t, x_ana) |
| 161 | + |
| 162 | + axes[0, 0].plot(x_ana, rho_ana, "r-", lw=2, label="Analytical") |
| 163 | + axes[0, 1].plot(x_ana, vx_ana, "r-", lw=2, label="Analytical") |
| 164 | + axes[1, 0].plot(x_ana, P_ana, "r-", lw=2, label="Analytical") |
| 165 | + |
| 166 | + # Density |
| 167 | + axes[0, 0].scatter(x_sort, rho_sort, s=1, alpha=0.5, label=solver_name) |
| 168 | + axes[0, 0].set_ylabel("Density") |
| 169 | + axes[0, 0].set_title("Density") |
| 170 | + axes[0, 0].legend() |
| 171 | + axes[0, 0].set_xlim(-1.1, 1.1) |
| 172 | + axes[0, 0].set_ylim(0, 1.2) |
| 173 | + |
| 174 | + # Velocity |
| 175 | + axes[0, 1].scatter(x_sort, vx_sort, s=1, alpha=0.5, label=solver_name) |
| 176 | + axes[0, 1].set_ylabel("Velocity") |
| 177 | + axes[0, 1].set_title("Velocity") |
| 178 | + axes[0, 1].legend() |
| 179 | + axes[0, 1].set_xlim(-1.1, 1.1) |
| 180 | + axes[0, 1].set_ylim(-0.1, 1.1) |
| 181 | + |
| 182 | + # Pressure |
| 183 | + axes[1, 0].scatter(x_sort, P_sort, s=1, alpha=0.5, label=solver_name) |
| 184 | + axes[1, 0].set_ylabel("Pressure") |
| 185 | + axes[1, 0].set_xlabel("x") |
| 186 | + axes[1, 0].set_title("Pressure") |
| 187 | + axes[1, 0].legend() |
| 188 | + axes[1, 0].set_xlim(-1.1, 1.1) |
| 189 | + axes[1, 0].set_ylim(0, 1.2) |
| 190 | + |
| 191 | + # Smoothing length |
| 192 | + axes[1, 1].scatter(x_sort, h_sort, s=1, alpha=0.5) |
| 193 | + axes[1, 1].set_ylabel("h") |
| 194 | + axes[1, 1].set_xlabel("x") |
| 195 | + axes[1, 1].set_title("Smoothing Length h") |
| 196 | + axes[1, 1].set_xlim(-1.1, 1.1) |
| 197 | + |
| 198 | + fig.suptitle( |
| 199 | + f"{solver_name} Sod Shock Tube (t = {t:.3f})", |
| 200 | + fontsize=14, |
| 201 | + fontweight="bold", |
| 202 | + ) |
| 203 | + plt.tight_layout() |
| 204 | + |
| 205 | + return axes.flat |
| 206 | + |
| 207 | + # Create animation |
| 208 | + print("Creating animation...") |
| 209 | + anim = FuncAnimation(fig, update, frames=len(vtk_files), interval=100) |
| 210 | + |
| 211 | + # Save as GIF |
| 212 | + solver_lower = solver_name.lower() |
| 213 | + gif_path = os.path.join(output_dir, f"{solver_lower}_sod_animation.gif") |
| 214 | + os.makedirs(output_dir, exist_ok=True) |
| 215 | + print(f"Saving to {gif_path}...") |
| 216 | + anim.save(gif_path, writer=PillowWriter(fps=args.fps)) |
| 217 | + print(f"Animation saved to {gif_path}") |
| 218 | + |
| 219 | + # Save final frame as PNG |
| 220 | + print("Saving final frame...") |
| 221 | + update(len(vtk_files) - 1) |
| 222 | + final_path = os.path.join(output_dir, f"{solver_lower}_sod_final.png") |
| 223 | + plt.savefig(final_path, dpi=150) |
| 224 | + print(f"Final frame saved to {final_path}") |
| 225 | + |
| 226 | + print() |
| 227 | + print(f"{'=' * 70}") |
| 228 | + print("Done!") |
| 229 | + print(f"{'=' * 70}") |
| 230 | + |
| 231 | + |
| 232 | +if __name__ == "__main__": |
| 233 | + main() |
0 commit comments