fela-power-grid / modeling.py
itstheraj's picture
initial commit
3476d8e
Raw
History Blame Contribute Delete
5.75 kB
import json
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
CONFIG = None
def _config():
global CONFIG
if CONFIG is None:
here = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(here, "config.json")) as f:
CONFIG = json.load(f)
return CONFIG
class RevIN(nn.Module):
def __init__(self, C):
super().__init__()
self.g = nn.Parameter(torch.ones(C))
self.b = nn.Parameter(torch.zeros(C))
def norm(self, x):
self.m = x.mean(1, keepdim=True)
self.s = x.std(1, keepdim=True) + 1e-05
return (x - self.m) / self.s * self.g + self.b
class FNO1D(nn.Module):
def __init__(self, D, modes):
super().__init__()
self.modes = modes
s = 1 / (D * D)
self.w = nn.Parameter(s * torch.rand(modes, D, D, dtype=torch.cfloat))
def forward(self, x):
P = x.shape[1]
xf = torch.fft.rfft(x, dim=1)
m = min(self.modes, xf.shape[1])
o = torch.zeros_like(xf)
o[:, :m] = torch.einsum("bpd,pde->bpe", xf[:, :m], self.w[:m])
return torch.fft.irfft(o, n=P, dim=1)
class Block(nn.Module):
def __init__(self, D, modes, ff=2, drop=0.0):
super().__init__()
self.n1 = nn.LayerNorm(D)
self.fno = FNO1D(D, modes)
self.d1 = nn.Dropout(drop)
self.n2 = nn.LayerNorm(D)
self.ff = nn.Sequential(
nn.Linear(D, D * ff), nn.GELU(), nn.Dropout(drop), nn.Linear(D * ff, D)
)
def forward(self, x):
x = x + self.d1(self.fno(self.n1(x)))
return x + self.ff(self.n2(x))
class FELA_Grid(nn.Module):
def __init__(self, Fin, L, D=96, modes=6, nblk=3, nq=99, arch="dual"):
super().__init__()
self.L = L
self.arch = arch
self.center = L // 2
self.nq = nq
self.revin = RevIN(Fin)
self.embed = nn.Linear(Fin, D)
self.pos = nn.Parameter(0.02 * torch.randn(1, L, D))
self.blocks = nn.ModuleList([Block(D, modes) for _ in range(nblk)])
self.norm = nn.LayerNorm(D)
if arch == "dual":
self.direct = nn.Sequential(
nn.Linear(Fin, D), nn.GELU(), nn.Linear(D, D), nn.GELU()
)
fuse_in = 2 * D
else:
fuse_in = D
self.med = nn.Linear(fuse_in, 1)
self.spread = nn.Linear(fuse_in, nq)
self.register_buffer("qidx", torch.arange(nq))
def forward(self, x):
xc = x[:, self.center]
xn = self.revin.norm(x)
h = self.embed(xn) + self.pos
for b in self.blocks:
h = b(h)
ctx = self.norm(h)[:, self.center]
if self.arch == "dual":
z = torch.cat([ctx, self.direct(xc)], dim=1)
else:
z = ctx
med = torch.sigmoid(self.med(z))
w = F.softplus(self.spread(z))
half = self.nq // 2
below = -torch.flip(torch.cumsum(torch.flip(w[:, :half], [1]), 1), [1])
above = torch.cumsum(w[:, half + 1 :], 1)
offs = (
torch.cat([below, torch.zeros_like(w[:, half : half + 1]), above], dim=1)
* 0.05
)
return torch.clamp(med + offs, 0, 1)
def expected_shape(track):
t = _config()["tracks"][track]
return (t["input_steps"], t["input_features"])
def validate_input(x, track):
steps, feats = expected_shape(track)
if not isinstance(x, torch.Tensor):
raise TypeError(f"Expected a torch.Tensor, got {type(x)}")
if x.dim() != 3:
raise ValueError(
f"{track}: expected a 3-D tensor (batch, steps, features), got shape {tuple(x.shape)}"
)
if x.shape[1] != steps or x.shape[2] != feats:
raise ValueError(
f"{track}: expected window (batch, {steps}, {feats}), got {tuple(x.shape)}. Solar windows are (.,6,20); wind windows are (.,12,15)."
)
return x
def preprocess_nwp(raw_window, track, mean=None, std=None):
x = torch.as_tensor(raw_window, dtype=torch.float32)
if x.dim() == 2:
x = x.unsqueeze(0)
if mean is not None and std is not None:
mean = torch.as_tensor(mean, dtype=torch.float32)
std = torch.as_tensor(std, dtype=torch.float32)
x = (x - mean) / torch.clamp(std, min=1e-06)
else:
m = x.mean(dim=(0, 1), keepdim=True)
s = x.std(dim=(0, 1), keepdim=True)
x = (x - m) / torch.clamp(s, min=1e-06)
return validate_input(x, track)
def _build_from_state(state, track):
dims = _config()["tracks"][track]["dims"]
model = FELA_Grid(
dims["Fin"],
dims["L"],
D=dims["D"],
modes=dims["modes"],
nblk=dims["nblk"],
nq=dims["nq"],
arch=dims.get("arch", "dual"),
)
ref = model.state_dict()
fixed = {}
for k, v in state.items():
t = ref.get(k)
if t is not None and t.is_complex() and (not v.is_complex()):
v = torch.view_as_complex(v.contiguous())
fixed[k] = v
fixed["qidx"] = model.qidx.clone()
model.load_state_dict(fixed, strict=True)
return model
def load_model(path_or_repo, track="solar", filename=None):
path = path_or_repo
fname = filename or _config()["tracks"][track]["weights_safetensors"]
if os.path.isdir(path):
path = os.path.join(path, fname)
elif not os.path.exists(path):
from huggingface_hub import hf_hub_download
path = hf_hub_download(path_or_repo, fname)
from safetensors.torch import load_file
state = load_file(path)
model = _build_from_state(state, track)
model.eval()
return model
def from_pretrained(repo_id, track="solar"):
return load_model(repo_id, track=track)