| import argparse |
| import sys |
| from pathlib import Path |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT)) |
|
|
| import math |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
| import torch |
| import yaml |
|
|
|
|
| def load_field(path, time_index=0): |
| """Return a prediction as [C, H, W] from common model output layouts.""" |
| value = torch.load(path, map_location="cpu", weights_only=False) |
| if isinstance(value, dict): |
| for key in ("prediction", "predictions", "output", "outputs"): |
| if key in value: |
| value = value[key] |
| break |
| field = torch.as_tensor(value).detach().cpu().float().numpy() |
| if field.ndim == 5: |
| field = field[0, time_index] |
| elif field.ndim == 4: |
| field = field[0 if field.shape[0] == 1 else time_index] |
| elif field.ndim != 3: |
| raise ValueError(f"Expected [C,H,W], [B,C,H,W], or [B,T,C,H,W], got {field.shape}") |
| if field.ndim != 3: |
| raise ValueError(f"Selected output is not [C,H,W]: {field.shape}") |
| return field |
|
|
|
|
| def load_channel_names(config_path, channel_count): |
| if config_path is None: |
| return [f"channel_{index}" for index in range(channel_count)] |
| with open(config_path, encoding="utf-8") as handle: |
| config = yaml.safe_load(handle) |
| names = config.get("data", {}).get("channels", []) |
| if len(names) != channel_count: |
| return [f"channel_{index}" for index in range(channel_count)] |
| return names |
|
|
|
|
| def choose_channels(names, requested, max_panels): |
| if requested: |
| selected = [] |
| for item in requested: |
| if item.isdigit(): |
| index = int(item) |
| if not 0 <= index < len(names): |
| raise ValueError(f"Channel index out of range: {index}") |
| else: |
| if item not in names: |
| raise ValueError(f"Unknown channel: {item}") |
| index = names.index(item) |
| if index not in selected: |
| selected.append(index) |
| return selected |
| return list(range(min(max_panels, len(names)))) |
|
|
|
|
| def is_signed_channel(name): |
| return name.startswith(("u_", "v_")) or name.startswith("ssh_") |
|
|
|
|
| def plot_fields(field, names, indices, output, title, reference=None): |
| columns = min(3, len(indices)) |
| rows = math.ceil(len(indices) / columns) |
| has_reference = reference is not None |
| fig, axes = plt.subplots(rows, columns, figsize=(5.6 * columns, 4.4 * rows), squeeze=False) |
| axes = axes.ravel() |
| height, width = field.shape[-2:] |
| longitude = np.linspace(0, 360, width, endpoint=False) |
| latitude = np.linspace(90, -90, height) |
| extent = [longitude[0], longitude[-1], latitude[-1], latitude[0]] |
|
|
| for axis, index in zip(axes, indices): |
| data = field[index] |
| reference_data = reference[index] if has_reference else None |
| if reference_data is not None: |
| data_min = min(np.nanpercentile(data, 2), np.nanpercentile(reference_data, 2)) |
| data_max = max(np.nanpercentile(data, 98), np.nanpercentile(reference_data, 98)) |
| else: |
| data_min, data_max = np.nanpercentile(data, [2, 98]) |
| if np.isclose(data_min, data_max): |
| data_min, data_max = float(np.nanmin(data)), float(np.nanmax(data) + 1e-6) |
| cmap = "RdBu_r" if is_signed_channel(names[index]) else "viridis" |
| image = axis.imshow(data, extent=extent, origin="upper", cmap=cmap, |
| vmin=data_min, vmax=data_max, aspect="auto") |
| axis.set_title(names[index], fontsize=11, fontweight="bold") |
| axis.set_xlabel("Longitude (degrees)") |
| axis.set_ylabel("Latitude (degrees)") |
| axis.set_xticks([0, 90, 180, 270, 360]) |
| axis.set_yticks([-90, -45, 0, 45, 90]) |
| axis.grid(color="white", linewidth=0.35, alpha=0.35) |
| colorbar = fig.colorbar(image, ax=axis, fraction=0.046, pad=0.04) |
| colorbar.ax.tick_params(labelsize=8) |
| stats = f"min {np.nanmin(data):.3g} | max {np.nanmax(data):.3g} | mean {np.nanmean(data):.3g}" |
| if reference_data is not None: |
| rmse = np.sqrt(np.nanmean((data - reference_data) ** 2)) |
| stats += f" | RMSE {rmse:.3g}" |
| axis.text(0.02, 0.02, stats, transform=axis.transAxes, fontsize=8, |
| color="white", bbox={"facecolor": "black", "alpha": 0.55, "pad": 3}) |
|
|
| for axis in axes[len(indices):]: |
| axis.remove() |
| fig.suptitle(title, fontsize=15, fontweight="bold") |
| fig.tight_layout() |
| fig.savefig(output, dpi=180, bbox_inches="tight") |
| plt.close(fig) |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--input", default=str(ROOT / "result/glonet/data/prediction.pt")) |
| parser.add_argument("--output", default=str(ROOT / "result/glonet/prediction.png")) |
| parser.add_argument("--config", default=str(ROOT / "conf/config.yaml")) |
| parser.add_argument("--reference", default=None, help="Optional .pt truth field for RMSE comparison") |
| parser.add_argument("--channel", action="append", help="Channel name or zero-based index; repeatable") |
| parser.add_argument("--max-panels", type=int, default=6) |
| parser.add_argument("--time-index", type=int, default=0) |
| args = parser.parse_args() |
| prediction = load_field(args.input, args.time_index) |
| names = load_channel_names(args.config, prediction.shape[0]) |
| indices = choose_channels(names, args.channel, args.max_panels) |
| reference = load_field(args.reference, args.time_index) if args.reference else None |
| if reference is not None and reference.shape != prediction.shape: |
| raise ValueError(f"Prediction/reference shape mismatch: {prediction.shape} vs {reference.shape}") |
| Path(args.output).parent.mkdir(parents=True, exist_ok=True) |
| plot_fields(prediction, names, indices, args.output, |
| f"GLONET ocean forecast | {len(indices)} channel(s)", reference) |
| print(f"saved={args.output}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|