Download make_figures.py from ElBalor/AlienX-3D-ISN-Operator: direct link, hf CLI and curl.
- Browser
- Download file 7.34 kB
-
https://huggingface.co/ElBalor/AlienX-3D-ISN-Operator/resolve/main/make_figures.py
- Command line
-
hf download hf://ElBalor/AlienX-3D-ISN-Operator/make_figures.py
-
curl -L -o make_figures.py https://huggingface.co/ElBalor/AlienX-3D-ISN-Operator/resolve/main/make_figures.py
7.34 kB
| # Generates paper figures from the released checkpoint + regenerated DNS. | |
| # stage A: rollouts for Re=300, 628 -> rollout_arrays_<Re>.npz | |
| # stage B: rollouts for Re=900, 1200 -> rollout_arrays_<Re>.npz | |
| # stage plot: renders the three paper figures (300 DPI, serif, grid) | |
| # Figures: | |
| # fig1_mse_growth.png - rollout pointwise MSE vs time, all Re, log-y | |
| # fig2_invariants.png - kinetic energy + enstrophy: model vs DNS (2 panels) | |
| # fig3_divergence.png - |div u| vs time, log-y, all Re | |
| import os, math, pickle, argparse | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| import sys | |
| BASE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, os.path.join(BASE, 'hf_upload')) | |
| from model import ISNOperator3D, get_isotropic_knn_3d_periodic | |
| import physics as ph | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--stage', default='plot', choices=['A', 'B', 'plot']) | |
| ARGS = parser.parse_args() | |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| with open(os.path.join(BASE, 'hf_upload', 'alienx_k5_stats.pkl'), 'rb') as f: | |
| SD = pickle.load(f) | |
| ST = SD['ST']; RE_LIST = SD['RE_LIST']; RE_MAX = SD['RE_MAX'] | |
| G = SD['G']; T_FINAL = SD['T_FINAL']; N_SNAPSHOTS = SD['N_SNAPSHOTS'] | |
| model = ISNOperator3D(hidden_dim=128, num_layers=4, num_heads=4, | |
| re_max=RE_MAX).to(device) | |
| model.G = G | |
| model.load_state_dict(torch.load(os.path.join(BASE, 'hf_upload', 'alienx_k5_best.pt'), | |
| map_location=device)) | |
| model.eval() | |
| KX, KY, KZ, K2 = ph.make_wavenumbers(G, device) | |
| x = torch.arange(G, device=device, dtype=torch.float32) * (2*math.pi/G) | |
| X, Y, Z = torch.meshgrid(x, x, x, indexing='ij') | |
| coords = torch.stack([X.flatten(), Y.flatten(), Z.flatten()], dim=-1).unsqueeze(0) | |
| knn = get_isotropic_knn_3d_periodic(G, device, dilation=max(1, G//16)) | |
| def _s(v, key): | |
| mu, sg = ST[key]; return (v - mu) / sg | |
| def _u(v, key): | |
| mu, sg = ST[key]; return v * sg + mu | |
| def rollout_arrays(Re): | |
| """Returns per-step arrays: mse, E_model, E_dns, Z_model, Z_dns, div.""" | |
| traj = ph.generate_tgv_dataset(G=G, nu=2*math.pi/Re, T=T_FINAL, dt=0.005, | |
| n_samples=N_SNAPSHOTS, device=device) | |
| u = traj.to(device) | |
| omega_traj = ph.velocity_to_vorticity(u, KX, KY, KZ) | |
| u_curr = u[0:1].clone() | |
| om_curr = omega_traj[0:1].clone() | |
| U0 = ph.rms_velocity(u_curr).clamp(min=1e-6) | |
| mses, Em, Ed, Zm, Zd, dv = [], [], [], [], [], [] | |
| for step in range(u.shape[0] - 1): | |
| om_nd = om_curr / U0 | |
| u_nd = u_curr / U0 | |
| k_, gm, gv, Hs = ph.scalar_derivatives(om_nd.norm(dim=-1), KX, KY, KZ) | |
| re_t = torch.tensor([float(Re)], device=device) | |
| pred = model(coords, _s(k_, 'k'), _s(gv, 'gv'), _s(gm, 'gm'), _s(Hs, 'H'), | |
| _s(om_nd.reshape(1, -1, 3), 'om'), | |
| _s(u_nd.reshape(1, -1, 3), 'u'), re_t, knn, G=G) | |
| om_next = _u(pred.reshape(1, -1, 3), 'tgt').reshape(1, G, G, G, 3) * U0 | |
| u_curr = ph.vorticity_to_velocity(om_next, KX, KY, KZ, K2) | |
| om_curr = om_next | |
| u_true = u[step+1:step+2] | |
| mses.append(F.mse_loss(om_curr, omega_traj[step+1:step+2]).item()) | |
| Em.append(ph.kinetic_energy(u_curr).item()) | |
| Ed.append(ph.kinetic_energy(u_true).item()) | |
| Zm.append(ph.enstrophy(u_curr, KX, KY, KZ).item()) | |
| Zd.append(ph.enstrophy(u_true, KX, KY, KZ).item()) | |
| dv.append(ph.divergence_spectral(u_curr, KX, KY, KZ).abs().mean().item()) | |
| if device.type == 'cuda': | |
| torch.cuda.empty_cache() | |
| return {k: np.array(v) for k, v in | |
| dict(mse=mses, Em=Em, Ed=Ed, Zm=Zm, Zd=Zd, div=dv).items()} | |
| if ARGS.stage in ('A', 'B'): | |
| todo = [300, 628] if ARGS.stage == 'A' else [900, 1200] | |
| for Re in todo: | |
| out = os.path.join(BASE, f'rollout_arrays_{Re}.npz') | |
| if os.path.exists(out): | |
| print(f'Re={Re}: cached') | |
| continue | |
| print(f'Re={Re}: rolling out 73 steps ...') | |
| arrs = rollout_arrays(Re) | |
| np.savez(out, **arrs) | |
| print(f' saved {out} (final MSE {arrs["mse"][-1]:.5f}, max div {arrs["div"].max():.2e})') | |
| raise SystemExit | |
| # ββ plot βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| from matplotlib import rcParams | |
| rcParams.update({ | |
| 'font.family': 'serif', | |
| 'font.size': 11, | |
| 'axes.grid': True, | |
| 'grid.alpha': 0.3, | |
| 'axes.spines.top': False, | |
| 'axes.spines.right': False, | |
| }) | |
| COLORS = {300: '#2c7bb6', 628: '#abd9e9', 900: '#f46d43', 1200: '#d7191c'} | |
| LABELS = {300: 'Re=300 (trained)', 628: 'Re=628 (trained)', | |
| 900: 'Re=900 (held-out)', 1200: 'Re=1200 (trained)'} | |
| DT = T_FINAL / (N_SNAPSHOTS - 1) | |
| t = np.arange(1, N_SNAPSHOTS) * DT | |
| data = {Re: dict(np.load(os.path.join(BASE, f'rollout_arrays_{Re}.npz'))) | |
| for Re in [300, 628, 900, 1200]} | |
| # Figure 1: MSE growth | |
| fig, ax = plt.subplots(figsize=(6.5, 4.2)) | |
| for Re in [300, 628, 900, 1200]: | |
| m = data[Re]['mse'] | |
| ax.plot(t, np.maximum(m, 1e-8), color=COLORS[Re], lw=1.8, label=LABELS[Re]) | |
| ax.set_yscale('log') | |
| ax.set_xlabel('physical time t') | |
| ax.set_ylabel('pointwise MSE (model vs DNS, vorticity)') | |
| ax.set_title('Autoregressive rollout error over the full trajectory (73 steps)', fontsize=11) | |
| ax.legend(frameon=False, fontsize=9) | |
| fig.tight_layout() | |
| fig.savefig(os.path.join(BASE, 'fig1_mse_growth.png'), dpi=300) | |
| plt.close(fig) | |
| # Figure 2: invariants, model vs DNS β 2 panels | |
| fig, axes = plt.subplots(1, 2, figsize=(10.5, 4.2)) | |
| for ax, (km, kd, ylab, title) in zip( | |
| axes, | |
| [('Em', 'Ed', 'kinetic energy E(t)', 'Kinetic energy'), | |
| ('Zm', 'Zd', 'enstrophy Z(t)', 'Enstrophy')]): | |
| for Re in [300, 628, 900, 1200]: | |
| ax.plot(t, data[Re][kd], color=COLORS[Re], lw=2.4, alpha=0.9) | |
| ax.plot(t, data[Re][km], color=COLORS[Re], lw=1.3, linestyle='--') | |
| ax.set_xlabel('physical time t') | |
| ax.set_ylabel(ylab) | |
| ax.set_title(title, fontsize=11) | |
| from matplotlib.lines import Line2D | |
| legend = [Line2D([0], [0], color=COLORS[r], lw=2.4, label=LABELS[r]) for r in [300, 628, 900, 1200]] | |
| legend += [Line2D([0], [0], color='gray', lw=2.4, label='DNS (solver)'), | |
| Line2D([0], [0], color='gray', lw=1.3, linestyle='--', label='AlienX 3D (model)')] | |
| fig.legend(handles=legend, loc='lower center', ncol=3, frameon=False, fontsize=9) | |
| fig.tight_layout(rect=[0, 0.08, 1, 1]) | |
| fig.savefig(os.path.join(BASE, 'fig2_invariants.png'), dpi=300) | |
| plt.close(fig) | |
| # Figure 3: divergence | |
| fig, ax = plt.subplots(figsize=(6.5, 4.2)) | |
| for Re in [300, 628, 900, 1200]: | |
| ax.plot(t, np.maximum(data[Re]['div'], 1e-16), color=COLORS[Re], lw=1.8, label=LABELS[Re]) | |
| ax.set_yscale('log') | |
| ax.set_ylim(1e-8, 1e-4) | |
| ax.set_xlabel('physical time t') | |
| ax.set_ylabel(r'$|\nabla \cdot u|$ (mean abs)') | |
| ax.set_title('Divergence during rollout β free by construction (BiotβSavart projection)', fontsize=11) | |
| ax.legend(frameon=False, fontsize=9) | |
| fig.tight_layout() | |
| fig.savefig(os.path.join(BASE, 'fig3_divergence.png'), dpi=300) | |
| plt.close(fig) | |
| print('figures written: fig1_mse_growth.png, fig2_invariants.png, fig3_divergence.png') | |