| """Evaluate DINCAE, residual calibration, and a rank-13 DINEOF-like baseline.""" |
|
|
| import json |
| from pathlib import Path |
| import sys |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import yaml |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT)) |
|
|
| from model.dincae import reconstruction_metrics |
|
|
|
|
| def dineof_like(observed, ocean_mask, requested_rank): |
| matrix = observed.reshape(observed.shape[0], -1).astype(np.float64) |
| sea = ocean_mask.ravel() |
| values = matrix[:, sea] |
| missing = ~np.isfinite(values) |
| column_mean = np.nanmean(values, axis=0) |
| column_mean = np.nan_to_num(column_mean, nan=float(np.nanmean(values))) |
| filled = np.where(missing, column_mean[None], values) |
| rank = min(int(requested_rank), min(filled.shape) - 1) |
| for _ in range(4): |
| mean = filled.mean(axis=0, keepdims=True) |
| u, singular, vt = np.linalg.svd(filled - mean, full_matrices=False) |
| reconstructed = (u[:, :rank] * singular[:rank]) @ vt[:rank] + mean |
| filled[missing] = reconstructed[missing] |
| result = np.full_like(matrix, np.nan) |
| result[:, sea] = filled |
| return result.reshape(observed.shape).astype(np.float32), rank |
|
|
|
|
| def main(): |
| config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) |
| with np.load(ROOT / config["paths"]["inference"]) as loaded: |
| pred = {key: loaded[key] for key in loaded.files} |
| with np.load(ROOT / config["data"]["root"] / config["data"]["file"]) as loaded: |
| full = {key: loaded[key] for key in loaded.files} |
| mask = pred["missing_mask"] |
| metrics = {"dincae": reconstruction_metrics(pred["target"], pred["prediction"], mask)} |
| std_residual = (pred["prediction"] - pred["target"]) / np.sqrt(np.maximum(pred["variance"], 1e-12)) |
| residual = std_residual[mask & np.isfinite(std_residual)] |
| bins = int(config["evaluation"]["calibration_bins"]) |
| edges = np.linspace(-4, 4, bins + 1) |
| counts, _ = np.histogram(residual, bins=edges) |
| metrics["standardized_residual_calibration"] = { |
| "mean": float(residual.mean()), "std": float(residual.std()), |
| "within_1sigma": float(np.mean(np.abs(residual) <= 1)), |
| "within_2sigma": float(np.mean(np.abs(residual) <= 2)), |
| "histogram_edges": edges.tolist(), "histogram_counts": counts.tolist(), |
| } |
| baseline_all, effective_rank = dineof_like(full["observed_anomaly"] + full["climatology"], |
| full["ocean_mask"], config["evaluation"]["dineof_rank"]) |
| start = min(int(config["data"]["train_samples"]), len(full["timestamps"]) - 1) |
| baseline = baseline_all[start:] |
| metrics["dineof_like"] = reconstruction_metrics(pred["target"], baseline, mask) |
| metrics["dineof_like"]["requested_rank"] = int(config["evaluation"]["dineof_rank"]) |
| metrics["dineof_like"]["effective_rank"] = effective_rank |
| out_dir = ROOT / config["paths"]["evaluation_dir"] |
| out_dir.mkdir(parents=True, exist_ok=True) |
| (out_dir / "metrics.json").write_text(json.dumps(metrics, indent=2)) |
| index = 0 |
| fields = (pred["observed"][index], pred["target"][index], pred["prediction"][index], |
| baseline[index], pred["prediction"][index] - pred["target"][index]) |
| titles = ("Cloudy AVHRR-like input", "Complete target", "DINCAE", "DINEOF-like rank 13", "DINCAE error") |
| fig, axes = plt.subplots(1, 5, figsize=(17, 3.6), constrained_layout=True) |
| lower, upper = np.nanpercentile(pred["target"][index], (2, 98)) |
| for axis, field, title in zip(axes, fields, titles): |
| if title.endswith("error"): |
| limit = max(float(np.nanpercentile(np.abs(field), 98)), 1e-3) |
| image = axis.imshow(field, cmap="RdBu_r", vmin=-limit, vmax=limit) |
| else: |
| image = axis.imshow(field, cmap="turbo", vmin=lower, vmax=upper) |
| axis.set_title(title, fontsize=9) |
| axis.set_axis_off() |
| fig.colorbar(image, ax=axis, fraction=0.046) |
| fig.savefig(out_dir / "comparison.png", dpi=150) |
| plt.close(fig) |
| print(json.dumps(metrics, indent=2)) |
| print(f"comparison={out_dir / 'comparison.png'}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|