| """Evaluate actual RainNet rollout outputs and create diagnostic figures.""" |
|
|
| import json |
| from pathlib import Path |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
| import yaml |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
|
|
| def load_config(): |
| with (ROOT / "conf/config.yaml").open(encoding="utf-8") as handle: |
| return yaml.safe_load(handle) |
|
|
|
|
| def csi(prediction, target, threshold): |
| pred_event, target_event = prediction >= threshold, target >= threshold |
| hits = np.logical_and(pred_event, target_event).sum(dtype=np.float64) |
| false_alarms = np.logical_and(pred_event, ~target_event).sum(dtype=np.float64) |
| misses = np.logical_and(~pred_event, target_event).sum(dtype=np.float64) |
| denominator = hits + false_alarms + misses |
| return float(hits / denominator) if denominator else 0.0 |
|
|
|
|
| def fss(prediction, target, threshold, window): |
| pred = torch.from_numpy((prediction >= threshold).astype(np.float32))[None, None] |
| obs = torch.from_numpy((target >= threshold).astype(np.float32))[None, None] |
| if window > 1: |
| padding = window // 2 |
| pred = F.avg_pool2d(pred, window, stride=1, padding=padding) |
| obs = F.avg_pool2d(obs, window, stride=1, padding=padding) |
| pred = pred[..., : prediction.shape[0], : prediction.shape[1]] |
| obs = obs[..., : target.shape[0], : target.shape[1]] |
| numerator = torch.sum((pred - obs) ** 2) |
| denominator = torch.sum(pred**2) + torch.sum(obs**2) |
| return float(1.0 - numerator / denominator) if denominator > 0 else 0.0 |
|
|
|
|
| def metric_series(predictions, targets, thresholds, windows): |
| output = [] |
| for index, (prediction, target) in enumerate(zip(predictions, targets)): |
| pred_rate, target_rate = prediction * 12.0, target * 12.0 |
| output.append( |
| { |
| "lead_minutes": (index + 1) * 5, |
| "mae_mm_h": float(np.mean(np.abs(pred_rate - target_rate), dtype=np.float64)), |
| "csi": {str(t): csi(pred_rate, target_rate, t) for t in thresholds}, |
| "fss": { |
| str(t): {str(w): fss(pred_rate, target_rate, t, w) for w in windows} |
| for t in thresholds |
| }, |
| } |
| ) |
| return output |
|
|
|
|
| def main(): |
| config = load_config() |
| output_dir = ROOT / config["inference"]["output_dir"] |
| required = ["inputs.npy", "predictions.npy", "targets.npy", "persistence.npy"] |
| missing = [name for name in required if not (output_dir / name).exists()] |
| if missing: |
| raise FileNotFoundError(f"Missing inference outputs: {missing}") |
| inputs = np.load(output_dir / "inputs.npy") |
| predictions = np.load(output_dir / "predictions.npy") |
| targets = np.load(output_dir / "targets.npy") |
| persistence = np.load(output_dir / "persistence.npy") |
| for name, array in (("prediction", predictions), ("target", targets), ("persistence", persistence)): |
| if array.shape != predictions.shape or not np.isfinite(array).all(): |
| raise ValueError(f"Invalid {name}: shape={array.shape}, finite={np.isfinite(array).all()}") |
| thresholds = config["evaluation"]["thresholds_mm_h"] |
| windows = config["evaluation"]["fss_windows_km"] |
| rainnet_metrics = metric_series(predictions, targets, thresholds, windows) |
| persistence_metrics = metric_series(persistence, targets, thresholds, windows) |
| metrics = { |
| "units": "mm/h", |
| "rainnet": rainnet_metrics, |
| "persistence": persistence_metrics, |
| } |
| result_dir = ROOT / config["evaluation"]["result_dir"] |
| result_dir.mkdir(parents=True, exist_ok=True) |
| with (result_dir / "metrics.json").open("w", encoding="utf-8") as handle: |
| json.dump(metrics, handle, indent=2) |
|
|
| history_path = result_dir / "train_history.json" |
| with history_path.open(encoding="utf-8") as handle: |
| history = json.load(handle) |
| fig, ax = plt.subplots(figsize=(6, 4)) |
| ax.plot(history["train_loss"], marker="o", label="Train") |
| ax.plot(history["validation_loss"], marker="o", label="Validation") |
| ax.set(xlabel="Epoch", ylabel="Log-Cosh loss", title="RainNet smoke training") |
| ax.legend() |
| fig.tight_layout() |
| fig.savefig(result_dir / "loss.png", dpi=150) |
| plt.close(fig) |
|
|
| selected = [0, 5, 11] |
| fig, axes = plt.subplots(3, 5, figsize=(16, 10)) |
| for row, index in enumerate(selected): |
| panels = [inputs[-1], targets[index], predictions[index], persistence[index], predictions[index] - targets[index]] |
| titles = ["Last Input", "Truth", "RainNet Prediction", "Persistence", "Prediction Error"] |
| for axis, panel, title in zip(axes[row], panels, titles): |
| image = axis.imshow(panel, cmap="RdBu_r" if title == "Prediction Error" else "Blues") |
| axis.set_title(f"{title}\n{(index + 1) * 5} min") |
| axis.axis("off") |
| fig.colorbar(image, ax=axis, fraction=0.046) |
| fig.tight_layout() |
| fig.savefig(result_dir / "forecast_comparison.png", dpi=120) |
| plt.close(fig) |
|
|
| leads = [item["lead_minutes"] for item in rainnet_metrics] |
| fig, axes = plt.subplots(1, 3, figsize=(15, 4)) |
| axes[0].plot(leads, [item["mae_mm_h"] for item in rainnet_metrics], label="RainNet") |
| axes[0].plot(leads, [item["mae_mm_h"] for item in persistence_metrics], label="Persistence") |
| axes[0].set(title="MAE", xlabel="Lead time (min)", ylabel="mm/h") |
| for threshold in thresholds: |
| axes[1].plot(leads, [item["csi"][str(threshold)] for item in rainnet_metrics], label=str(threshold)) |
| axes[2].plot(leads, [item["fss"][str(threshold)]["20"] for item in rainnet_metrics], label=str(threshold)) |
| axes[1].set(title="RainNet CSI", xlabel="Lead time (min)", ylabel="CSI") |
| axes[2].set(title="RainNet FSS (20 km)", xlabel="Lead time (min)", ylabel="FSS") |
| axes[0].legend() |
| axes[1].legend(title="mm/h", fontsize=7) |
| axes[2].legend(title="mm/h", fontsize=7) |
| fig.tight_layout() |
| fig.savefig(result_dir / "metrics.png", dpi=150) |
| plt.close(fig) |
|
|
| print(f"Prediction shape: {predictions.shape}") |
| print(f"Target shape: {targets.shape}") |
| print(f"Persistence shape: {persistence.shape}") |
| for index in selected: |
| item = rainnet_metrics[index] |
| print(f"Lead {item['lead_minutes']} min MAE: {item['mae_mm_h']:.8f} mm/h") |
| print(f"Lead {item['lead_minutes']} min CSI: {item['csi']}") |
| print(f"Lead {item['lead_minutes']} min FSS: {item['fss']}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|