PINN / Variant_5 BaseFlow /animate_cfd.py
inaniloquentee's picture
Upload folder using huggingface_hub
7fa74f9 verified
Raw
History Blame Contribute Delete
8.8 kB
import torch
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from mpl_toolkits.axes_grid1 import make_axes_locatable
from scipy.spatial import cKDTree
import os
from dataset import CFDReconstructionDataset
from architectures import PIGU_Hybrid
from model import NavierStokesURANS
# ==========================================
# 1. 动画与模型配置区 (专为 Variant 5 设置)
# ==========================================
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
SENSOR_COUNT = 65
DT = 0.05
FRAMES_TO_RENDER = 60
# 指向 Variant 5 的权重路径和输出文件
MODEL_WEIGHTS_PATH = "results_ablation_v5/model_ep400.pth"
OUTPUT_FILENAME = "eval_results/var5_animation_with_metrics.gif"
PATH_UNSTEADY = "../../Ablation Experiment/dataset_sin/flow_one_period.npy"
PATH_MEAN = "../../Ablation Experiment/dataset_sin/mean_flow_steady.npy"
VAR_IDX = 0
VAR_NAME = ['u-velocity', 'v-velocity', 'Pressure'][VAR_IDX]
def main():
os.makedirs(os.path.dirname(OUTPUT_FILENAME), exist_ok=True)
print(f"[*] Initializing CFD Animation & Evaluation for Variant 5...")
dataset = CFDReconstructionDataset(PATH_UNSTEADY, PATH_MEAN, SENSOR_COUNT, dt=DT)
stats_max = dataset.stats['max'].to(DEVICE)
stats_min = dataset.stats['min'].to(DEVICE)
dx_val = dataset.box_len[0] / (dataset.W - 1)
dy_val = dataset.box_len[1] / (dataset.H - 1)
dx_tensor = torch.tensor(dx_val, device=DEVICE)
dy_tensor = torch.tensor(dy_val, device=DEVICE)
def denormalize_batch(norm_tensor):
return (norm_tensor + 1) / 2 * (stats_max - stats_min) + stats_min
print(f"[*] Loading Model Weights from: {MODEL_WEIGHTS_PATH}")
model = PIGU_Hybrid(sensor_in_dim=3, sensor_count=SENSOR_COUNT).to(DEVICE)
if not os.path.exists(MODEL_WEIGHTS_PATH):
print(f"[!] Warning: Weights {MODEL_WEIGHTS_PATH} not found. Please train V5 first.")
return
model.load_state_dict(torch.load(MODEL_WEIGHTS_PATH, map_location=DEVICE))
model.eval()
pde_engine = NavierStokesURANS(dataset.stats).to(DEVICE)
# 提取传感器的物理坐标用于绘图
sensor_phys_x = [dataset.grid_X[p[0], p[1]] for p in dataset.sensor_indices]
sensor_phys_y = [dataset.grid_Y[p[0], p[1]] for p in dataset.sensor_indices]
print("[*] Building Geometry Mask...")
raw_mean = np.load(PATH_MEAN).astype(np.float32)
coords_mean = raw_mean[:, :2]
tree = cKDTree(coords_mean)
grid_pts = np.stack([dataset.grid_X.ravel(), dataset.grid_Y.ravel()], axis=-1)
dist, _ = tree.query(grid_pts)
threshold = max(dx_val, dy_val) * 2.5
is_solid_wall = (dist > threshold).reshape(dataset.H, dataset.W)
is_solid_wall_float = is_solid_wall.astype(float)
valid_mask_torch = ~torch.from_numpy(is_solid_wall).to(DEVICE)
print(f"[*] Running Inference and Calculating Metrics for {FRAMES_TO_RENDER} frames...")
true_frames, pred_frames, err_frames = [], [], []
total_l2 = 0.0
total_rmse = 0.0
total_resc = 0.0
num_frames = min(FRAMES_TO_RENDER, len(dataset))
with torch.no_grad():
for idx in range(num_frames):
s_val_t, s_pos, grid_pos_norm, s_val_next, mean_flow = dataset[idx]
true_norm_t = dataset.data[idx]
s_val_t = s_val_t.unsqueeze(0).to(DEVICE)
s_pos = s_pos.unsqueeze(0).to(DEVICE)
grid_pos_norm = grid_pos_norm.unsqueeze(0).to(DEVICE)
s_val_next = s_val_next.unsqueeze(0).to(DEVICE)
mean_flow = mean_flow.unsqueeze(0).to(DEVICE)
true_norm_t = true_norm_t.unsqueeze(0).to(DEVICE)
pred_norm_t = model(s_val_t, s_pos, grid_pos_norm, base_flow=mean_flow)
pred_norm_next = model(s_val_next, s_pos, grid_pos_norm, base_flow=mean_flow)
pred_phys_t = denormalize_batch(pred_norm_t)
true_phys_t = denormalize_batch(true_norm_t)
pred_phys_next = denormalize_batch(pred_norm_next)
p_var_torch = pred_phys_t[0, VAR_IDX]
t_var_torch = true_phys_t[0, VAR_IDX]
diff_valid = (p_var_torch - t_var_torch)[valid_mask_torch]
t_valid = t_var_torch[valid_mask_torch]
frame_l2 = torch.norm(diff_valid) / (torch.norm(t_valid) + 1e-8)
frame_rmse = torch.sqrt(torch.mean(diff_valid**2))
_, _, res_c = pde_engine(pred_phys_t, pred_phys_next, None, dx=dx_tensor, dy=dy_tensor)
res_c_valid = res_c[0, 0][valid_mask_torch]
frame_resc = torch.sqrt(torch.mean(res_c_valid**2))
total_l2 += frame_l2.item()
total_rmse += frame_rmse.item()
total_resc += frame_resc.item()
p_var = p_var_torch.cpu().numpy()
t_var = t_var_torch.cpu().numpy()
p_var[is_solid_wall] = np.nan
t_var[is_solid_wall] = np.nan
err_var = np.abs(p_var - t_var)
true_frames.append(t_var)
pred_frames.append(p_var)
err_frames.append(err_var)
avg_l2 = total_l2 / num_frames
avg_rmse = total_rmse / num_frames
avg_resc = total_resc / num_frames
print(f"\n[Metrics] L2 Rel Error: {avg_l2:.4f} | RMSE: {avg_rmse:.4f} | Cont. Res: {avg_resc:.2e}")
# ==========================================
# 3. 顶级学术期刊 CFD 风格动画渲染
# ==========================================
print(f"[*] Generating Professional CFD-Style Animation...")
vmin = np.nanmin(true_frames)
vmax = np.nanmax(true_frames)
err_vmax = np.nanmax(err_frames) * 0.8
fig, axes = plt.subplots(1, 3, figsize=(18, 5.5), facecolor='white')
fig.subplots_adjust(bottom=0.2)
fig.suptitle(f'Variant 5 (w/o Base Flow) Reconstruction - {VAR_NAME}', fontsize=18, fontweight='bold', y=0.98)
extent = [dataset.grid_X.min(), dataset.grid_X.max(), dataset.grid_Y.min(), dataset.grid_Y.max()]
for ax in axes:
ax.set_facecolor('#c0c0c0')
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
im_true = axes[0].imshow(true_frames[0], cmap='turbo', origin='lower', extent=extent, vmin=vmin, vmax=vmax, interpolation='bicubic')
axes[0].contour(is_solid_wall_float, levels=[0.5], extent=extent, colors='black', linewidths=1.5, origin='lower')
axes[0].set_title("CFD Ground Truth", fontsize=14, pad=10)
im_pred = axes[1].imshow(pred_frames[0], cmap='turbo', origin='lower', extent=extent, vmin=vmin, vmax=vmax, interpolation='bicubic')
axes[1].contour(is_solid_wall_float, levels=[0.5], extent=extent, colors='black', linewidths=1.5, origin='lower')
axes[1].set_title("Direct Full-Flow Prediction", fontsize=14, pad=10)
axes[1].scatter(sensor_phys_x, sensor_phys_y, c='white', edgecolors='black', s=18, alpha=0.9, zorder=5)
divider = make_axes_locatable(axes[1])
cax = divider.append_axes("right", size="5%", pad=0.1)
fig.colorbar(im_pred, cax=cax, label=f'{VAR_NAME} Magnitude')
im_err = axes[2].imshow(err_frames[0], cmap='inferno', origin='lower', extent=extent, vmin=0, vmax=err_vmax, interpolation='bicubic')
axes[2].contour(is_solid_wall_float, levels=[0.5], extent=extent, colors='white', linewidths=1.5, origin='lower')
axes[2].set_title("Absolute Error", fontsize=14, pad=10)
divider_err = make_axes_locatable(axes[2])
cax_err = divider_err.append_axes("right", size="5%", pad=0.1)
fig.colorbar(im_err, cax=cax_err, label='Error Magnitude')
metrics_text = (
f"Quantitative Evaluation ({FRAMES_TO_RENDER} frames average)\n"
f"$L_2$ Relative Error: {avg_l2:.4f} | RMSE: {avg_rmse:.4f} | Continuity Residual: {avg_resc:.2e}"
)
fig.text(0.5, 0.05, metrics_text, ha='center', va='center', fontsize=14, fontweight='bold',
bbox=dict(facecolor='#f8f9fa', alpha=0.9, edgecolor='#adb5bd', boxstyle='round,pad=0.5'))
def update(frame_idx):
im_true.set_array(true_frames[frame_idx])
im_pred.set_array(pred_frames[frame_idx])
im_err.set_array(err_frames[frame_idx])
return [im_true, im_pred, im_err]
ani = animation.FuncAnimation(fig, update, frames=len(true_frames), interval=100, blit=True)
if OUTPUT_FILENAME.endswith('.mp4'):
ani.save(OUTPUT_FILENAME, writer='ffmpeg', fps=10, dpi=200)
else:
ani.save(OUTPUT_FILENAME, writer='pillow', fps=10, dpi=150)
print(f"[+] Animation successfully saved to: {OUTPUT_FILENAME}")
if __name__ == "__main__":
main()