"""Compact consistency model for full-grid precipitation downscaling.""" import json from pathlib import Path import numpy as np import torch from torch import nn import torch.nn.functional as F import yaml def load_config(root): return yaml.safe_load((Path(root) / "conf/config.yaml").read_text()) class TimeBlock(nn.Module): def __init__(self, cin, cout, time_dim): super().__init__() groups = min(4, cout) self.conv = nn.Conv2d(cin, cout, 3, padding=1) self.norm = nn.GroupNorm(groups, cout) self.time = nn.Linear(time_dim, cout) def forward(self, x, embedding): return F.silu(self.norm(self.conv(x)) + self.time(embedding)[:, :, None, None]) class ScaleAdaptiveCM(nn.Module): def __init__(self, channels=(4, 8, 16), time_dim=16, sigma_data=0.5): super().__init__() self.sigma_data = float(sigma_data) self.time_dim = int(time_dim) self.time_mlp = nn.Sequential(nn.Linear(time_dim, time_dim), nn.SiLU(), nn.Linear(time_dim, time_dim)) self.enc1 = TimeBlock(1, channels[0], time_dim) self.enc2 = TimeBlock(channels[0], channels[1], time_dim) self.mid = TimeBlock(channels[1], channels[2], time_dim) self.dec2 = TimeBlock(channels[2] + channels[1], channels[1], time_dim) self.dec1 = TimeBlock(channels[1] + channels[0], channels[0], time_dim) self.out = nn.Conv2d(channels[0], 1, 1) self.model_config = {"channels": list(channels), "time_dim": time_dim, "sigma_data": sigma_data} def embed_time(self, t): half = self.time_dim // 2 freq = torch.exp(torch.linspace(0, -7, half, device=t.device)) emb = torch.cat((torch.sin(t[:, None] * freq), torch.cos(t[:, None] * freq)), 1) return self.time_mlp(emb) def forward(self, noisy, t): if noisy.ndim != 4 or noisy.shape[1] != 1: raise ValueError("expected [B,1,H,W]") emb = self.embed_time(t.float()) e1 = self.enc1(noisy, emb) e2 = self.enc2(F.avg_pool2d(e1, 2), emb) mid = self.mid(F.avg_pool2d(e2, 2), emb) d2 = self.dec2(torch.cat((F.interpolate(mid, e2.shape[-2:], mode="bilinear", align_corners=False), e2), 1), emb) d1 = self.dec1(torch.cat((F.interpolate(d2, e1.shape[-2:], mode="bilinear", align_corners=False), e1), 1), emb) raw = self.out(d1) sigma2 = self.sigma_data ** 2 cskip = sigma2 / ((t[:, None, None, None] - 0.002).square() + sigma2) cout = self.sigma_data * t[:, None, None, None] / torch.sqrt(t[:, None, None, None].square() + sigma2) return cskip * noisy + cout * raw def structured_fields(samples, high_h, high_w, seed): rng = np.random.default_rng(seed) yy, xx = np.mgrid[-1:1:complex(high_h), -1:1:complex(high_w)] fields = [] for i in range(samples): phase = 2 * np.pi * i / max(samples, 4) itcz = 9 * np.exp(-((yy - .12 * np.sin(phase)) / .16) ** 2) storms = 18 * np.exp(-((xx - .45 * np.cos(phase)) ** 2 + (yy - .3 * np.sin(phase)) ** 2) / .025) texture = 2 * np.maximum(0, np.sin(18 * xx + phase) * np.cos(13 * yy - phase)) fields.append(np.maximum(0, itcz + storms + texture + rng.normal(0, .15, yy.shape))) return np.asarray(fields, np.float32)[:, None] def radial_spectrum(field): power = np.abs(np.fft.fftshift(np.fft.fft2(field))) ** 2 y, x = np.indices(field.shape); r = np.sqrt((y-field.shape[0]/2)**2 + (x-field.shape[1]/2)**2).astype(int) return np.bincount(r.ravel(), power.ravel()) / np.maximum(np.bincount(r.ravel()), 1) def write_json(path, value): path = Path(path); path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, indent=2) + "\n")