| """Evaluate dense PINN-TC predictions and create sparse/target/prediction/error panels.""" |
|
|
| import importlib.util |
| import json |
| from pathlib import Path |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import torch |
| import yaml |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
|
|
| def load_model_module(): |
| spec = importlib.util.spec_from_file_location("pinn_tc_model", ROOT / "model/pinn-tc.py") |
| module = importlib.util.module_from_spec(spec) |
| spec.loader.exec_module(module) |
| return module |
|
|
|
|
| def main(): |
| config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) |
| module = load_model_module() |
| archive = np.load(ROOT / config["paths"]["prediction"]) |
| if str(archive["format_version"]) != config["data"]["format_version"]: |
| raise ValueError("prediction format_version mismatch") |
| prediction = archive["predictions"] |
| expected = (251, 251, 44, 3, 4) |
| if prediction.shape != expected or prediction.dtype != np.float32: |
| raise ValueError(f"prediction must be float32 with shape {expected}") |
| y_axis, x_axis, pressures, times = archive["y"], archive["x"], archive["pressure"], archive["time"] |
| sum_speed_error = sum_pred = sum_true = sum_pred2 = sum_true2 = sum_cross = 0.0 |
| count = 0 |
| radial_edges = np.linspace(0.0, np.sqrt(2.0) * config["data"]["horizontal_extent_m"], 13) |
| radial_sse = np.zeros(len(radial_edges) - 1); radial_count = np.zeros(len(radial_edges) - 1, dtype=np.int64) |
| yy, xx = np.meshgrid(y_axis, x_axis, indexing="ij") |
| radius = np.sqrt(xx * xx + yy * yy) |
| for pressure_index, pressure in enumerate(pressures): |
| for time_index, time in enumerate(times): |
| coordinates = np.column_stack((yy.ravel(), xx.ravel(), np.full(yy.size, time), np.full(yy.size, pressure))).astype(np.float32) |
| target = module.analytic_vortex_numpy(coordinates).reshape(251, 251, 4) |
| predicted_speed = np.linalg.norm(prediction[:, :, pressure_index, time_index, :2].astype(np.float64), axis=-1) |
| true_speed = np.linalg.norm(target[:, :, :2].astype(np.float64), axis=-1) |
| difference = predicted_speed - true_speed |
| sum_speed_error += np.square(difference).sum(); count += difference.size |
| sum_pred += predicted_speed.sum(); sum_true += true_speed.sum() |
| sum_pred2 += np.square(predicted_speed).sum(); sum_true2 += np.square(true_speed).sum() |
| sum_cross += (predicted_speed * true_speed).sum() |
| bins = np.clip(np.digitize(radius.ravel(), radial_edges) - 1, 0, len(radial_sse) - 1) |
| radial_sse += np.bincount(bins, weights=np.square(difference).ravel(), minlength=len(radial_sse)) |
| radial_count += np.bincount(bins, minlength=len(radial_count)) |
| covariance = sum_cross - sum_pred * sum_true / count |
| variance_pred = sum_pred2 - sum_pred * sum_pred / count |
| variance_true = sum_true2 - sum_true * sum_true / count |
| pearson = covariance / np.sqrt(max(variance_pred * variance_true, 1e-30)) |
|
|
| checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location="cpu", weights_only=False) |
| if checkpoint.get("format_version") != config["data"]["format_version"]: |
| raise ValueError("checkpoint format_version mismatch") |
| model = module.PINNTC(**checkpoint["model_config"]); model.load_state_dict(checkpoint["model"]); model.eval() |
| rng = np.random.default_rng(int(config["seed"]) + 99) |
| residual_count = int(config["evaluation"]["pde_points"]) |
| residual_coordinates = np.column_stack((rng.uniform(-400_000, 400_000, (residual_count, 2)), |
| rng.uniform(-21_600, 21_600, residual_count), |
| rng.uniform(15_000, 90_000, residual_count))).astype(np.float32) |
| tensor = torch.from_numpy(residual_coordinates).requires_grad_(True) |
| ru, rv, rc = module.pde_residuals(model, tensor, **config["physics"]) |
| report = {"dense_shape": list(expected), "dense_points": int(np.prod(expected[:-1])), |
| "wind_speed_rmse_m_s": float(np.sqrt(sum_speed_error / count)), "wind_speed_pearson": float(pearson), |
| "pde_residual_rms": {"ru_m_s2": float(torch.sqrt(torch.mean(ru.square())).detach()), |
| "rv_m_s2": float(torch.sqrt(torch.mean(rv.square())).detach()), |
| "rc_s-1": float(torch.sqrt(torch.mean(rc.square())).detach())}, |
| "radial_rmse": [{"radius_mid_km": float((left + right) / 2000.0), "rmse_m_s": float(np.sqrt(error / max(samples, 1))), "count": int(samples)} |
| for left, right, error, samples in zip(radial_edges[:-1], radial_edges[1:], radial_sse, radial_count)], |
| "scope": "Structured analytic engineering validation, not a paper-performance claim."} |
| values = [report["wind_speed_rmse_m_s"], report["wind_speed_pearson"], *report["pde_residual_rms"].values()] |
| if not np.isfinite(values).all(): |
| raise FloatingPointError("evaluation produced a non-finite metric") |
| output = ROOT / config["paths"]["evaluation_dir"] |
| output.mkdir(parents=True, exist_ok=True) |
| (output / "metrics.json").write_text(json.dumps(report, indent=2) + "\n") |
|
|
| pressure_index, time_index = int(np.argmin(abs(pressures - 50_000))), int(np.argmin(abs(times))) |
| coordinates = np.column_stack((yy.ravel(), xx.ravel(), np.full(yy.size, times[time_index]), |
| np.full(yy.size, pressures[pressure_index]))).astype(np.float32) |
| target_speed = np.linalg.norm(module.analytic_vortex_numpy(coordinates)[:, :2], axis=1).reshape(251, 251) |
| predicted_speed = np.linalg.norm(prediction[:, :, pressure_index, time_index, :2], axis=-1) |
| sparse = np.load(ROOT / config["data"]["root"] / "training_points.npz")["observation_coordinates"] |
| selected = sparse[(abs(sparse[:, 2] - times[time_index]) < 1) & (abs(sparse[:, 3] - pressures[pressure_index]) < 1000)] |
| figure, axes_plot = plt.subplots(1, 4, figsize=(16, 4), constrained_layout=True) |
| axes_plot[0].scatter(selected[:, 1] / 1000, selected[:, 0] / 1000, s=5, c="black"); axes_plot[0].set_title("Sparse observations") |
| panels = ((target_speed, "Target wind speed", "viridis"), (predicted_speed, "Predicted wind speed", "viridis"), |
| (predicted_speed - target_speed, "Prediction error", "coolwarm")) |
| for axis, (field, title, color_map) in zip(axes_plot[1:], panels): |
| image = axis.imshow(field, origin="lower", extent=(-400, 400, -400, 400), cmap=color_map) |
| axis.set_title(title); figure.colorbar(image, ax=axis, shrink=0.75) |
| for axis in axes_plot: |
| axis.set_xlabel("x (km)"); axis.set_ylabel("y (km)") |
| figure.savefig(output / "comparison.png", dpi=140); plt.close(figure) |
| print(f"evaluation={output.relative_to(ROOT)} wind_rmse={report['wind_speed_rmse_m_s']:.6g} pearson={pearson:.6g}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|