from __future__ import annotations import argparse import json import sys from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np from common import DEFAULT_CONFIG, load_config, resolve_path def latitude_weights(height: int) -> np.ndarray: latitude = np.linspace(np.pi / 2, -np.pi / 2, height) weights = np.cos(latitude).clip(min=0) return weights / weights.mean() def compute_metrics(prediction: np.ndarray, target: np.ndarray) -> dict[str, list[float]]: weights = latitude_weights(target.shape[-2])[None, None, None, :, None] error = prediction - target rmse = np.sqrt(np.mean(error**2 * weights, axis=(0, 1, 3, 4))) spatial_weight = weights / ( weights.sum(axis=(-2, -1), keepdims=True) * target.shape[-1] ) pred_mean = np.sum(prediction * spatial_weight, axis=(-2, -1), keepdims=True) target_mean = np.sum(target * spatial_weight, axis=(-2, -1), keepdims=True) pred_anomaly = prediction - pred_mean target_anomaly = target - target_mean numerator = np.sum(pred_anomaly * target_anomaly * weights, axis=(0, 1, 3, 4)) denominator = np.sqrt( np.sum(pred_anomaly**2 * weights, axis=(0, 1, 3, 4)) * np.sum(target_anomaly**2 * weights, axis=(0, 1, 3, 4)) ) acc = numerator / np.maximum(denominator, 1e-12) return {"rmse": rmse.tolist(), "acc": acc.tolist()} def plot_sample( prediction: np.ndarray, target: np.ndarray, variable: str, channel_index: int, cmap: str, output_path: Path, ) -> None: predicted = prediction[0, 0, channel_index] expected = target[0, 0, channel_index] error = predicted - expected value_min = min(predicted.min(), expected.min()) value_max = max(predicted.max(), expected.max()) error_limit = max(abs(error.min()), abs(error.max()), 1e-12) extent = (0, 360, -90, 90) figure, axes = plt.subplots(3, 1, figsize=(12, 10), constrained_layout=True) image = axes[0].imshow( expected, origin="upper", extent=extent, aspect="auto", cmap=cmap, vmin=value_min, vmax=value_max, ) axes[0].set_title(f"Target {variable}") figure.colorbar(image, ax=axes[0], orientation="vertical") image = axes[1].imshow( predicted, origin="upper", extent=extent, aspect="auto", cmap=cmap, vmin=value_min, vmax=value_max, ) axes[1].set_title(f"Prediction {variable}") figure.colorbar(image, ax=axes[1], orientation="vertical") image = axes[2].imshow( error, origin="upper", extent=extent, aspect="auto", cmap="RdBu_r", vmin=-error_limit, vmax=error_limit, ) axes[2].set_title(f"Error {variable}") figure.colorbar(image, ax=axes[2], orientation="vertical") for axis in axes: axis.set_xlabel("Longitude") axis.set_ylabel("Latitude") output_path.parent.mkdir(parents=True, exist_ok=True) figure.savefig(output_path, dpi=160) plt.close(figure) def main() -> None: parser = argparse.ArgumentParser(description="Evaluate and plot FCNv2 output") parser.add_argument("--config", default=str(DEFAULT_CONFIG)) parser.add_argument("--input") args = parser.parse_args() config = load_config(args.config) inference_dir = resolve_path(config, config["inference"]["output_dir"]) input_path = Path(args.input).expanduser().resolve() if args.input else None files = [input_path] if input_path else sorted(inference_dir.glob("sample_*.npz")) if not files: raise FileNotFoundError(f"No inference outputs found in {inference_dir}") predictions = [] targets = [] for path in files: with np.load(path) as data: predictions.append(data["prediction"]) targets.append(data["target"]) prediction = np.concatenate(predictions) target = np.concatenate(targets) metrics = compute_metrics(prediction, target) output_dir = resolve_path(config, config["visualization"]["output_dir"]) output_dir.mkdir(parents=True, exist_ok=True) (output_dir / "metrics.json").write_text( json.dumps(metrics, indent=2), encoding="utf-8" ) variable = config["visualization"]["variable"] channel_index = config["data"]["variables"].index(variable) sample_index = config["visualization"]["sample_index"] if not 0 <= sample_index < prediction.shape[0]: raise IndexError( f"visualization.sample_index={sample_index} is outside " f"the available range [0, {prediction.shape[0] - 1}]" ) plot_sample( prediction[sample_index : sample_index + 1], target[sample_index : sample_index + 1], variable, channel_index, config["visualization"]["cmap"], output_dir / f"{variable}_forecast.png", ) print(output_dir / "metrics.json") print(output_dir / f"{variable}_forecast.png") if __name__ == "__main__": main()