"""Tile diagnostics for analysis, forecasts, variable groups, and localization.""" import argparse import json from pathlib import Path import matplotlib.pyplot as plt import numpy as np import torch import sys, yaml ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from model.fuxi_da import CompactForecastProxy, FuXiDA, make_sample GROUPS = {"Z": slice(0, 13), "T": slice(13, 26), "U": slice(26, 39), "V": slice(39, 52), "R": slice(52, 65), "surface": slice(65, 70)} def weighted_rmse(prediction, target, latitude): weight = torch.cos(torch.deg2rad(latitude)).clamp_min(0) weight = weight / weight.sum() return torch.sqrt(((prediction - target).square() * weight[None, :, None]).sum(dim=(-2, -1)) / prediction.shape[-1]).mean().item() def main(): parser = argparse.ArgumentParser() parser.add_argument("--config", default="conf/config.yaml") parser.add_argument("--checkpoint") parser.add_argument("--output") parser.add_argument("--metrics") args = parser.parse_args() cfg = yaml.safe_load((ROOT / args.config).read_text()) model = FuXiDA(cfg["model"]["base_channels"]) checkpoint_path = Path(args.checkpoint) if args.checkpoint else ROOT / cfg["paths"]["checkpoint"] if checkpoint_path.exists(): model.load_state_dict(torch.load(checkpoint_path, map_location="cpu", weights_only=True)["model"]) model.eval() proxy = CompactForecastProxy() rows = [] with torch.no_grad(): for tile_id in cfg["data"]["tile_ids"]: sample = make_sample(tile_id, 20, cfg["data"]["tile_size"], cfg["data"]["missing_probability"]) analysis = model(sample["background"][None], sample["obs"][None])[0] row = {"tile_id": tile_id, "origin": sample["origin"].tolist(), "background": weighted_rmse(sample["background"], sample["target"], sample["latitude"]), "correction": weighted_rmse(sample["correction"], sample["target"], sample["latitude"]), "analysis": weighted_rmse(analysis, sample["target"], sample["latitude"])} row["variable_groups"] = {name: weighted_rmse(analysis[index], sample["target"][index], sample["latitude"]) for name, index in GROUPS.items()} state = analysis row["forecast_steps"] = [] for lead in range(cfg["train"]["forecast_steps"]): state = proxy(state[None])[0] row["forecast_steps"].append(weighted_rmse(state, sample["forecast_targets"][lead], sample["latitude"])) rows.append(row) sample = make_sample(cfg["data"]["tile_ids"][0], 21, cfg["data"]["tile_size"], 0.0) base = model(sample["background"][None], sample["obs"][None])[0] perturbed_obs = sample["obs"].clone() center = cfg["data"]["tile_size"] // 2 perturbed_obs[9 - 8, center, center] += 1.0 response = (model(sample["background"][None], perturbed_obs[None])[0] - base).square().sum(0) yy, xx = torch.meshgrid(torch.arange(cfg["data"]["tile_size"]), torch.arange(cfg["data"]["tile_size"]), indexing="ij") radius = torch.sqrt((yy - center).float().square() + (xx - center).float().square()) total_energy = response.sum().clamp_min(1e-12) localization = {"perturbation": "AGRI channel 9 +1 K at tile center", "energy_weighted_radius_gridpoints": (response * radius).sum().div(total_energy).item(), "energy_within_radius_4": response[radius <= 4].sum().div(total_energy).item()} metrics = {"coverage_complete": False, "claim": "diagnostic metrics on selected aligned tiles; not complete global scores", "tiles": rows, "increment_localization": localization} metrics_path = Path(args.metrics) if args.metrics else ROOT / cfg["paths"]["evaluation"] metrics_path.parent.mkdir(parents=True, exist_ok=True); metrics_path.write_text(json.dumps(metrics, indent=2) + "\n") labels = ["background", "correction", "analysis"] values = [np.mean([row[label] for row in rows]) for label in labels] fig, axes = plt.subplots(1, 2, figsize=(10, 4)) axes[0].bar(labels, values, color=["#596780", "#d69b36", "#287f71"]) axes[0].set_ylabel("Latitude-weighted RMSE") axes[0].set_title("Selected aligned tiles (incomplete coverage)") group_values = [np.mean([row["variable_groups"][name] for row in rows]) for name in GROUPS] axes[1].bar(list(GROUPS), group_values, color="#287f71") axes[1].set_title("Analysis RMSE by variable group") fig.suptitle("FuXi-DA procedural protocol diagnostics") fig.tight_layout() output_path = Path(args.output) if args.output else ROOT / cfg["paths"]["figure"] output_path.parent.mkdir(parents=True, exist_ok=True); fig.savefig(output_path, dpi=160) plt.close(fig) print(json.dumps(metrics, indent=2)) if __name__ == "__main__": main()