%load_ext autoreload
%autoreload 2
RWRAP-test#
Test du modèle warp + residual sur HEALPix pour la prévision SST.
Ce notebook reprend la préparation de données du notebook U-NET existant, mais remplace le HealPixUNet par HealPixAdvectionResidualNet.
Idée du modèle:
tête 1 : vitesse tangentielle est-ouest
utête 2 : vitesse tangentielle nord-sud
vtête 3 : résidu local
rincrément prédit :
ΔT = -(u ∂T/∂east + v ∂T/∂north) + r
Ici on garde la même cible que dans ton notebook préparé :
Y[t] = SST_norm[t+1] - SST_norm[t]
Donc le réseau est instancié avec output_mode="delta".
import numpy as np
import matplotlib.pyplot as plt
import torch
from healpix_plot.mollview import mollview
import healpix_geo as hg
from healpix_analyse.rwrap_healpix import HealPixAdvectionResidualNet
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Device: {DEVICE}")
sst_fields = np.load("../../Healpix_UNET/Notebooks/train_heal_32.npy")[:65*12, -1]
mollview(sst_fields[10], nest=True, cmap="coolwarm")
# Données océan uniquement
ocean_mask = np.isfinite(sst_fields[0]).astype(np.float32)
sst_ocean = np.where(np.isfinite(sst_fields), sst_fields, np.nan)
print("sst_ocean shape:", sst_ocean.shape)
# Standardisation mois par mois
mu = np.nanmean(sst_ocean.reshape(65, 12, sst_ocean.shape[1]), axis=0, keepdims=True)
std = np.nanstd(sst_ocean.reshape(65, 12, sst_ocean.shape[1]), axis=0, keepdims=True)
sst_norm = (sst_ocean.reshape(65, 12, sst_ocean.shape[1]) - mu) / std
sst_norm = sst_norm.reshape(65*12, sst_ocean.shape[1])
sst_norm[np.isnan(sst_norm)] = 0.0
sst_norm[:, ocean_mask == 0] = 0.0
print("norm shape:", sst_norm.shape)
# Input / target
# X[t] = [SST_norm(t), mask]
# Y[t] = SST_norm(t+1) - SST_norm(t)
X = np.stack([
sst_norm,
np.broadcast_to(ocean_mask, sst_norm.shape).copy(),
], axis=1).astype(np.float32)
X = X[:-1]
Y = (sst_norm[1:, None, :] - sst_norm[:-1, None, :]).astype(np.float32)
N_train = int(0.8 * Y.shape[0])
X_train, Y_train = X[:N_train], Y[:N_train]
X_val, Y_val = X[N_train:], Y[N_train:]
print(f"Train: {X_train.shape} / {Y_train.shape}")
print(f"Val : {X_val.shape} / {Y_val.shape}")
NSIDE = 32
depth = 5
enso_idx, _, _ = hg.nested.cone_coverage((130, 0), 5.0, depth)
_MASK_T = torch.as_tensor(ocean_mask, dtype=torch.float32, device=DEVICE)
def loss_ocean_delta(y_pred: torch.Tensor, y_true: torch.Tensor) -> torch.Tensor:
"""
Loss sur l'incrément SST, océan uniquement, avec régularisation
flow smoothness + petit résidu implicites dans model.total_loss.
"""
mask = _MASK_T.to(y_pred.device)
err = (y_pred - y_true) ** 2
data_term = (err * mask).sum() / (mask.sum() * y_pred.shape[0] * y_pred.shape[1])
# On ajoute les régularisations internes du modèle à l'extérieur
# pour garder une logique explicite dans le notebook.
return data_term + 0.05 * model.flow_smoothness_loss() + 0.01 * model.residual_l1_loss()
model = HealPixAdvectionResidualNet(
nside=NSIDE,
in_channels=2,
out_channels=1,
feature_channels=[16, 8, 4, 2],
vars_per_t=1,
time_steps=1,
down_mode="maxpool",
weight_norm="l1",
residual_clip=4.0,
gate_scale=1.0,
tau=1.0,
kernel_sz=5,
n_gauges=1,
gauge_type="two_ref",
singularity_lonlat=[
(-55.0, -10.0),
( 90.0, 45.0),
],
ellipsoid="WGS84",
grad_ring=1,
smoothness_lambda=0.05,
residual_lambda=0.01,
output_mode="delta",
device=DEVICE,
)
print(model)
print(f"\nModel parameters: {model.count_parameters():,}")
print("\n--- Training -------------------------------------------------------")
history = model.fit(
X_train[-250:], Y_train[-250:],
x_val=X_val, y_val=Y_val,
n_epoch=300,
batch_size=16,
lr=3e-3,
weight_decay=1e-5,
optimizer="adam",
view_epoch=1,
loss_fn=loss_ocean_delta,
)
# Evaluation sur incréments
Y_ptrain = model.predict(X_train, batch_size=16).numpy()[:, 0, :]
Y_ptrue = Y_train[:, 0, :]
Y_pred = model.predict(X_val, batch_size=16).numpy()[:, 0, :]
Y_true = Y_val[:, 0, :]
mask_np = ocean_mask.astype(bool)
mse_val = np.mean((Y_pred[:, mask_np] - Y_true[:, mask_np]) ** 2)
rmse_val = np.sqrt(mse_val)
corr_val = float(np.mean([
np.corrcoef(Y_pred[i, mask_np], Y_true[i, mask_np])[0, 1]
for i in range(Y_pred.shape[0])
]))
print(f"Validation RMSE: {rmse_val:.4f}")
print(f"Validation Corr: {corr_val:.4f}")
# Visualisation incrément prédit vs vrai
a = 2
plt.figure(figsize=(16, 4))
tmp = Y_ptrain[300] / np.where(ocean_mask > 0, ocean_mask, np.nan)
tmp[enso_idx] = a * 2
mollview(tmp, nest=True, hold=False, sub=(1, 2, 1), cmap="coolwarm", vmin=-a, vmax=a)
tmp = Y_ptrue[300] / np.where(ocean_mask > 0, ocean_mask, np.nan)
tmp[enso_idx] = a * 2
mollview(tmp, nest=True, hold=False, sub=(1, 2, 2), cmap="coolwarm", vmin=-a, vmax=a)
plt.figure(figsize=(16, 4))
tmp = Y_pred[70] / np.where(ocean_mask > 0, ocean_mask, np.nan)
tmp[enso_idx] = -a * 2
mollview(tmp, nest=True, hold=False, sub=(1, 2, 1), cmap="coolwarm", vmin=-a, vmax=a)
tmp = Y_true[70] / np.where(ocean_mask > 0, ocean_mask, np.nan)
tmp[enso_idx] = -a * 2
mollview(tmp, nest=True, hold=False, sub=(1, 2, 2), cmap="coolwarm", vmin=-a, vmax=a)
# Diagnostic des composantes du modèle
components = model.predict_components(X_val[:8], batch_size=8)
flow_e = components["flow_east"].numpy()[:, 0, :]
flow_n = components["flow_north"].numpy()[:, 0, :]
resid = components["residual"].numpy()[:, 0, :]
idx = 0
A = 2
plt.figure(figsize=(16, 9))
mollview(flow_e[idx] / np.where(ocean_mask > 0, ocean_mask, np.nan), nest=True, hold=False, sub=(2, 2, 1), cmap="coolwarm")
plt.title("flow east")
mollview(flow_n[idx] / np.where(ocean_mask > 0, ocean_mask, np.nan), nest=True, hold=False, sub=(2, 2, 2), cmap="coolwarm")
plt.title("flow north")
mollview(resid[idx] / np.where(ocean_mask > 0, ocean_mask, np.nan), nest=True, hold=False, sub=(2, 2, 3), cmap="coolwarm", vmin=-A, vmax=A)
plt.title("residual")
mollview(Y_pred[idx] / np.where(ocean_mask > 0, ocean_mask, np.nan), nest=True, hold=False, sub=(2, 2, 4), cmap="coolwarm", vmin=-A, vmax=A)
plt.title("predicted delta")
# Reconstruction de SST(t+1) absolue à partir de ΔT prédit
last_sst_val = X_val[:, 0, :]
SST_next_pred = last_sst_val + Y_pred
SST_next_true = last_sst_val + Y_true
plt.figure(figsize=(16, 4))
mollview(SST_next_pred[10] / np.where(ocean_mask > 0, ocean_mask, np.nan), nest=True, hold=False, sub=(1, 2, 1), cmap="coolwarm")
mollview(SST_next_true[10] / np.where(ocean_mask > 0, ocean_mask, np.nan), nest=True, hold=False, sub=(1, 2, 2), cmap="coolwarm")