GLONET / scripts /result.py
yzt15806542928's picture
Upload folder using huggingface_hub
1aeffbb verified
Raw
History Blame Contribute Delete
6.04 kB
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: # [B, T, C, H, W]
field = field[0, time_index]
elif field.ndim == 4: # [B, C, H, W] or [T, C, H, W]
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()