File size: 9,278 Bytes
0f0d1f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | #!/usr/bin/env python3
"""Reload best checkpoint; evaluate exact DNS and DNS-interpolated 200x200 grids."""
from __future__ import annotations
import argparse
import csv
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import numpy as np
import torch
from scipy.interpolate import RectBivariateSpline
from scripts.common import (ROOT, gather, load_checkpoint, load_dns, predict, read_config,
relative_l2, resolve, save_json, setup, tensor)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", default=str(ROOT / "config/config.yaml"))
parser.add_argument("--checkpoint")
parser.add_argument("--device")
args = parser.parse_args()
cfg = read_config(args.config)
device, dtype = setup(cfg, args.device)
model, checkpoint = load_checkpoint(cfg, device, args.checkpoint)
dns = load_dns(cfg)
if checkpoint["data_sha256"] != dns["sha256"]:
raise ValueError("Checkpoint was trained on different DNS bytes")
out = resolve(cfg["outputs"]["results"])
sampling = np.load(out / "sampling_indices.npz")
xy, times = dns["xy"], dns["t"]
nx, ny = cfg["evaluation"]["grid_size"]
gx = np.linspace(xy[:, 0].min(), xy[:, 0].max(), nx)
gy = np.linspace(xy[:, 1].min(), xy[:, 1].max(), ny)
xx, yy = np.meshgrid(gx, gy)
grid_xy = np.column_stack((xx.ravel(), yy.ravel()))
native_x, native_y = np.unique(xy[:, 0]), np.unique(xy[:, 1])
ix, iy = np.searchsorted(native_x, xy[:, 0]), np.searchsorted(native_y, xy[:, 1])
dns_grid_coincidence = np.any(np.isclose(gx[:, None], native_x[None, :], atol=1e-10, rtol=0), 1)[None, :] & np.any(np.isclose(gy[:, None], native_y[None, :], atol=1e-10, rtol=0), 1)[:, None]
grid_unseen = ~dns_grid_coincidence.ravel()
native_pred = np.empty((len(times), len(xy), 3), dtype=np.float64)
per_time, grid_sse, grid_denom = [], np.zeros(2), np.zeros(2)
unseen_sse, unseen_denom = np.zeros(2), np.zeros(2)
held_sse, held_denom = np.zeros(2), np.zeros(2)
snapshot = int(np.argmin(abs(times - cfg["evaluation"]["snapshot_time"])))
batch_size = cfg["evaluation"]["batch_size"]
native_heldout_count = 0
for k, t in enumerate(times):
q = np.column_stack((xy, np.full(len(xy), t)))
pred = predict(model, q, device, dtype, batch_size)
native_pred[k] = pred
errors = relative_l2(pred[:, :2], dns["uv"][k])
unused = np.ones(len(xy), dtype=bool)
unused[sampling["boundary"][k]] = False
unused[sampling["residual"][k]] = False
native_heldout_count += int(unused.sum())
held_sse += np.sum((pred[unused, :2] - dns["uv"][k, unused])**2, 0)
held_denom += np.sum(dns["uv"][k, unused]**2, 0)
gq = np.column_stack((grid_xy, np.full(len(grid_xy), t)))
gp = predict(model, gq, device, dtype, batch_size)[:, :2]
reference = np.empty((ny, nx, 2), dtype=np.float64)
for channel in range(2):
field = np.empty((len(native_y), len(native_x)))
field[iy, ix] = dns["uv"][k, :, channel]
reference[:, :, channel] = RectBivariateSpline(native_y, native_x, field, kx=3, ky=3)(gy, gx)
ref = reference.reshape(-1, 2)
ge = relative_l2(gp, ref)
grid_sse += np.sum((gp - ref)**2, 0)
grid_denom += np.sum(ref**2, 0)
unseen_sse += np.sum((gp[grid_unseen] - ref[grid_unseen])**2, 0)
unseen_denom += np.sum(ref[grid_unseen]**2, 0)
row = {"time_index": k, "time": float(t), "dns_u": errors[0], "dns_v": errors[1], "grid_u": ge[0], "grid_v": ge[1]}
per_time.append(row)
if k == snapshot:
np.savez_compressed(out / "snapshot_grid.npz", x=gx, y=gy, reference=reference,
prediction=gp.reshape(ny, nx, 2), time=t,
unseen_mask=grid_unseen.reshape(ny, nx))
if k % 10 == 0 or k == len(times) - 1:
print(f"INFERENCE time_index={k:3d}/199 t={t:.2f} DNS_relL2(u,v)=({errors[0]:.8e},{errors[1]:.8e}) grid_relL2(u,v)=({ge[0]:.8e},{ge[1]:.8e})", flush=True)
np.savez_compressed(out / "dns_predictions.npz", X_star=xy, t=times, uvp_prediction=native_pred,
uv_reference=dns["uv"], p_reference=dns["p"])
with (out / "per_time_metrics.csv").open("w", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=list(per_time[0]))
writer.writeheader()
writer.writerows(per_time)
primary = relative_l2(native_pred[:, :, :2].reshape(-1, 2), dns["uv"].reshape(-1, 2))
snap_error = np.array([per_time[snapshot]["dns_u"], per_time[snapshot]["dns_v"]])
grid_aggregate = np.sqrt(grid_sse / grid_denom)
grid_snapshot = np.array([per_time[snapshot]["grid_u"], per_time[snapshot]["grid_v"]])
table = cfg["evaluation"]["table3"]
table_values = np.array([table["tsa_u"], table["tsa_v"]])
nearest = int(np.argmin(np.sum((xy - np.array(cfg["evaluation"]["probe"]))**2, 1)))
probe_q = np.column_stack((np.tile(xy[nearest], (len(times), 1)), times))
probe_prediction = predict(model, probe_q, device, dtype, batch_size)
np.savez_compressed(out / "probe_series.npz", xy=xy[nearest], requested_xy=cfg["evaluation"]["probe"],
t=times, reference=dns["uv"][:, nearest], prediction=probe_prediction[:, :2])
diagnostics_q, _ = gather(dns, sampling["validation"])
diagnostic_count = min(len(diagnostics_q), 1000)
diagnostics_q = diagnostics_q[np.linspace(0, len(diagnostics_q) - 1, diagnostic_count, dtype=int)]
momentum_sse, div_sse = np.zeros(2), 0.0
ad_max_abs = {"flow": 0.0, "momentum": 0.0}
# Use the independent reverse-mode implementation for physical diagnostics.
for start in range(0, diagnostic_count, 250):
q = tensor(diagnostics_q[start:start + 250], device, dtype)
flow_values, residual, div = model.residual_autograd(q)
with torch.no_grad():
taylor_values, taylor_residual, _ = model.residual_taylor(q)
torch.testing.assert_close(taylor_values, flow_values, atol=1e-8, rtol=1e-7)
torch.testing.assert_close(taylor_residual, residual, atol=1e-8, rtol=1e-7)
ad_max_abs["flow"] = max(ad_max_abs["flow"], float((taylor_values-flow_values).abs().max().detach()))
ad_max_abs["momentum"] = max(ad_max_abs["momentum"], float((taylor_residual-residual).abs().max().detach()))
momentum_sse += (residual.detach().cpu().numpy()**2).sum(0)
div_sse += float((div.detach()**2).sum())
def pair(array):
return {"u": float(array[0]), "v": float(array[1])}
metrics = {"checkpoint_completed_updates": checkpoint["completed_updates"], "data_path": dns["path"],
"data_sha256": dns["sha256"], "metric": "||prediction-reference||_2 / ||reference||_2 (dimensionless ratio)",
"dns_all_spacetime": pair(primary), "dns_all_spacetime_percent": pair(primary * 100),
"dns_unused_spacetime": pair(np.sqrt(held_sse / held_denom)), "dns_unused_count": native_heldout_count,
"dns_snapshot": {"time": float(times[snapshot]), **pair(snap_error)},
"grid200_all_spacetime": pair(grid_aggregate), "grid200_snapshot": {"time": float(times[snapshot]), **pair(grid_snapshot)},
"grid200_strictly_unseen_spacetime": pair(np.sqrt(unseen_sse / unseen_denom)),
"grid200_unseen_spatial_count": int(grid_unseen.sum()),
"grid_reference": "Tensor-product cubic spline interpolation of this exact DNS; not additional DNS ground truth",
"table3_tsa_pinn": pair(table_values), "table3_pinn": {"u": table["pinn_u"], "v": table["pinn_v"]},
"snapshot_over_table3_ratio": pair(snap_error / table_values),
"grid_snapshot_over_table3_ratio": pair(grid_snapshot / table_values),
"all_spacetime_over_table3_ratio": pair(primary / table_values),
"probe": {"requested": cfg["evaluation"]["probe"], "actual_dns_point": xy[nearest].tolist(),
"relative_l2": pair(relative_l2(probe_prediction[:, :2], dns["uv"][:, nearest]))},
"physics_diagnostics": {"count": diagnostic_count, "sampling": "uniformly spaced across held-out locations over all 200 frames", "momentum_rmse": pair(np.sqrt(momentum_sse / diagnostic_count)),
"divergence_rmse": float(np.sqrt(div_sse / diagnostic_count)),
"trained_checkpoint_ad_equivalence": {"status": "passed", "max_absolute_difference": ad_max_abs}},
"comparison_limits": ["Table3 exact seed/checkpoint/evaluation aggregation not specified in paper", "Paper Eq42 includes x100, but Table3 numerical entries are reported without a percent marker; both ratio and percent provided", "Native DNS covers t=0..19.9 at dt=0.1; no synthetic temporal resampling", "PyTorch port of paper TensorFlow method; coordinate AD checked against reverse-mode PyTorch"]}
save_json(out / "metrics.json", metrics)
print("FINAL METRICS " + json.dumps(metrics, ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()
|