Download scripts/result.py from OneScience-Group/FluxNet-ShallowWater-LAP: direct link, hf CLI and curl.
- Browser
- Download file 9.42 kB
-
https://huggingface.co/OneScience-Group/FluxNet-ShallowWater-LAP/resolve/main/scripts/result.py
- Command line
-
hf download hf://OneScience-Group/FluxNet-ShallowWater-LAP/scripts/result.py
-
curl -L -o result.py https://huggingface.co/OneScience-Group/FluxNet-ShallowWater-LAP/resolve/main/scripts/result.py
9.42 kB
| """Create standalone scientific figures and audit actual inference artifacts.""" | |
| import json | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from common import FIELDS, ROOT, load_config, output_dir, parser, save_json, sha256 | |
| def main(): | |
| args = parser(__doc__).parse_args() | |
| cfg = load_config(args.config) | |
| out = output_dir(cfg) | |
| history = json.loads((out / "training_history.json").read_text()) | |
| status = json.loads((out / "training_status.json").read_text()) | |
| manifest = json.loads((out / "inference_manifest.json").read_text()) | |
| metrics = json.loads((out / "metrics.json").read_text()) | |
| assert status["completed_epochs"] == 300 and status["state"] == "completed" | |
| assert [r["epoch"] for r in history] == list(range(1, 301)) | |
| assert all(np.isfinite(r["train_loss"]) and np.isfinite(r["val_mse"]) for r in history) | |
| selected = min(history, key=lambda r: r["val_mse"]) | |
| assert selected["epoch"] == manifest["selected_epoch"] == status["best_epoch"] | |
| assert sha256(ROOT / cfg["paths"]["best_checkpoint"]) == manifest["sha256"] | |
| figures = [] | |
| plt.rcParams.update({"font.size": 10, "figure.dpi": 140, "savefig.dpi": 180}) | |
| fig, ax = plt.subplots(figsize=(7, 4)) | |
| for key, label in [("train_step1_mse", "Train step 1"), | |
| ("train_step5_mse", "Train step 5"), ("val_mse", "Validation step 1 + 5")]: | |
| ax.semilogy([r["epoch"] for r in history], [r[key] for r in history], label=label) | |
| ax.axvline(status["best_epoch"], color="0.6", lw=1, ls=":", label="Selected checkpoint") | |
| ax.set(xlabel="Epoch", ylabel="MSE in physical units", title="FluxNet-LAP: 300-epoch pushforward training") | |
| ax.legend() | |
| fig.tight_layout() | |
| fig.savefig(out / "training_curve.png") | |
| plt.close(fig) | |
| figures.append("training_curve.png") | |
| checks = {} | |
| for split in ("test", "test_long"): | |
| data = np.load(out / f"{split}.npz") | |
| expected_steps = cfg["data"]["expected_frames"][split] - 1 | |
| assert data["mae"].shape == (50, expected_steps, 3) | |
| assert np.isfinite(data["mae"]).all() | |
| assert np.allclose(data["mae"].mean((0, 1)), metrics[split]["mae"], atol=1e-12) | |
| assert np.allclose(data["mae"][:, -1].mean(0), metrics[split]["mae_final"], atol=1e-12) | |
| assert float(data["negative_h_ratio"].mean()) == metrics[split]["negative_h_ratio"] | |
| assert np.allclose(data["conservation_absolute_sum"].max((0, 1)), | |
| metrics[split]["conservation_absolute_sum_max"]) | |
| fig, axes = plt.subplots(1, 3, figsize=(13, 3.6)) | |
| for c, field in enumerate(FIELDS): | |
| axes[0].plot(data["time"], data["mae"].mean(0)[:, c], label=field) | |
| axes[1].plot(data["time"], data["conservation_absolute_sum"].max(0)[:, c], label=field) | |
| axes[0].set(xlabel="Time", ylabel="MAE", title="Autoregressive error") | |
| axes[1].set(xlabel="Time", ylabel="max |sum(u)-sum(u0)|", title="Conservation drift (float64 reduction)") | |
| axes[2].plot(data["time"], 100 * data["negative_h_ratio"].mean(0)) | |
| axes[2].set(xlabel="Time", ylabel="Negative depth (%)", title="Depth bound violations") | |
| for ax in axes[:2]: | |
| ax.legend() | |
| fig.tight_layout() | |
| name = f"{split}_rollout_metrics.png" | |
| fig.savefig(out / name) | |
| figures.append(name) | |
| plt.close(fig) | |
| examples = np.load(out / f"{split}_examples.npz") | |
| for case in cfg["evaluation"]["example_cases"]: | |
| prediction = examples[case + "_prediction"] | |
| target = examples[case + "_target"] | |
| assert prediction.shape == target.shape == (expected_steps + 1, 3, 64, 64) | |
| assert np.array_equal(prediction[0], target[0]) | |
| # Independent recomputation from saved physical fields. | |
| errors = np.abs(prediction[1:].astype(np.float64) - target[1:]).mean((0, 2, 3)) | |
| file = str(examples[case + "_file"]) | |
| index = list(data["files"]).index(file) | |
| assert np.allclose(errors, data["mae"][index].mean(0), atol=1e-12) | |
| frames = np.round(np.linspace(0, expected_steps, 6)).astype(int) | |
| for c, field in enumerate(FIELDS): | |
| fig, axes = plt.subplots(3, 6, figsize=(14, 7.3), layout="constrained") | |
| minimum = min(target[frames, c].min(), prediction[frames, c].min()) | |
| maximum = max(target[frames, c].max(), prediction[frames, c].max()) | |
| error_max = np.abs(prediction[frames, c] - target[frames, c]).max() | |
| for j, frame in enumerate(frames): | |
| for r, values in enumerate((target, prediction)): | |
| image = axes[r, j].imshow(values[frame, c], origin="lower", extent=(0, 10, 0, 10), | |
| cmap="viridis", vmin=minimum, vmax=maximum) | |
| err_image = axes[2, j].imshow(np.abs(prediction[frame, c] - target[frame, c]), | |
| origin="lower", extent=(0, 10, 0, 10), cmap="magma", | |
| vmin=0, vmax=max(float(error_max), 1e-12)) | |
| axes[0, j].set_title(f"t={examples['time'][frame]:.2f}") | |
| for r in range(3): | |
| axes[r, j].set_xticks([]) | |
| axes[r, j].set_yticks([]) | |
| for r, label in enumerate(("Reference", "FluxNet-LAP", "Absolute error")): | |
| axes[r, 0].set_ylabel(label) | |
| fig.colorbar(image, ax=axes[:2, :], shrink=0.75) | |
| fig.colorbar(err_image, ax=axes[2, :], shrink=0.75) | |
| fig.suptitle(f"{split} Case {case}: {field}") | |
| name = f"{split}_{case}_{field}.png" | |
| fig.savefig(out / name) | |
| figures.append(name) | |
| plt.close(fig) | |
| checks[split] = {"all_trajectories": True, "all_steps": True, "finite": True, | |
| "metrics_recomputed": True, "example_fields_recomputed": True, | |
| "zero_negative_depth": metrics[split]["negative_h_ratio"] == 0} | |
| report = ["# FluxNet shallow-water LAP pushforward reproduction", "", | |
| "Source: https://arxiv.org/pdf/2602.01941v2 (Sections 3.2–3.7, Appendix C.2).", "", | |
| "One independent training run (seed 42), 300 complete epochs. This is not a five-seed mean/std replication.", | |
| "64 channels, 6 residual blocks, kernel 5, transport stencil 3×3, detached pushforward length 5.", | |
| "AdamW lr=1e-3, weight_decay=1e-2; ReduceLROnPlateau factor=0.5, patience=15; float32 states.", "", | |
| f"Selected checkpoint epoch: {manifest['selected_epoch']}; validation MSE: {manifest['best_val_mse']:.8e}.", | |
| f"Checkpoint SHA256: `{manifest['sha256']}`.", "", | |
| "| Split | Horizon | MAE h | MAE mx | MAE my | Negative h (%) |", | |
| "| --- | ---: | ---: | ---: | ---: | ---: |"] | |
| for split, values in metrics.items(): | |
| report.append(f"| {split} | {values['t_end']:.1f} | " + " | ".join(f"{v:.8e}" for v in values["mae"]) + | |
| f" | {values['negative_h_percent']:.8e} |") | |
| report += ["", "MAE averages all forecast steps, grid cells, and 50 trajectories, excluding the initial frame.", | |
| "Final-time and T=2/T=2.4/T=4.8 prefix metrics are separately stored in metrics.json.", | |
| "The paper labels Table 3 as T=2 while Appendix C.1 specifies 2.4/4.8; both time conventions are retained.", | |
| "", "Conservation is computed using float64 reductions of float32 predictions relative to the initial state.", | |
| "Relative drift divides by |initial signed total|+1e-8; zero-total momentum makes this statistic ill-conditioned.", | |
| "Absolute sum drift and initial-L1-scaled drift are therefore also reported. No postprocessing correction is used.", ""] | |
| for split, values in metrics.items(): | |
| report += [f"{split} maximum absolute sum drift [h,mx,my]: {values['conservation_absolute_sum_max']}", | |
| f"{split} maximum relative drift [h,mx,my]: {values['conservation_relative_max']}", | |
| f"{split} minimum depth: {values['min_h']:.8e}", ""] | |
| report += ["All arrays, checkpoints, training history, and saved-case MAE recomputations were checked.", | |
| "The original numerical-generator datasets were read directly; no data generation or official repository code was downloaded."] | |
| report += ["", "Reference data contains small negative roundoff depths (about 1e-11); these are retained unchanged.", | |
| "Data validation tolerates reference values down to -1e-9; model violation metrics use the strict condition h<0, with no tolerance or clipping."] | |
| (out / "report.md").write_text("\n".join(report) + "\n") | |
| save_json(out / "verification.json", {"completed_epochs": 300, | |
| "best_checkpoint_hash_matches": True, "selected_epoch_is_val_minimum": True, | |
| "checkpoint_sha256": manifest["sha256"], "splits": checks, | |
| "figures": figures, "report": "report.md"}) | |
| print("RESULT_VERIFIED " + json.dumps({"completed_epochs": 300, "checks": checks, | |
| "figures": len(figures), "results": str(out)}), flush=True) | |
| if __name__ == "__main__": | |
| main() | |