| import json |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| from torch import nn |
|
|
|
|
| def load_config(path): |
| with open(path, encoding="utf-8") as handle: |
| return json.load(handle) |
|
|
|
|
| def write_json(path, value): |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| with open(path, "w", encoding="utf-8") as handle: |
| json.dump(value, handle, indent=2, sort_keys=True) |
|
|
|
|
| class StreamflowLSTM(nn.Module): |
| """Paper stack with explicit inter-layer activation semantics.""" |
|
|
| def __init__(self, input_size=23, hidden_size=50, dropout=0.1): |
| super().__init__() |
| self.layers = nn.ModuleList([ |
| nn.LSTM(input_size, hidden_size, batch_first=True), |
| nn.LSTM(hidden_size, hidden_size, batch_first=True), |
| nn.LSTM(hidden_size, hidden_size, batch_first=True), |
| ]) |
| self.dropout = nn.Dropout(dropout) |
| self.dense = nn.Linear(hidden_size, 1) |
|
|
| def forward(self, inputs): |
| if inputs.ndim != 3 or inputs.shape[-2:] != (28, 23): |
| raise ValueError(f"expected [B,28,23], got {tuple(inputs.shape)}") |
| values, _ = self.layers[0](inputs) |
| values = self.dropout(torch.relu(values)) |
| values, _ = self.layers[1](values) |
| values = self.dropout(torch.relu(values)) |
| values, _ = self.layers[2](values) |
| values = torch.tanh(values[:, -1]) |
| return self.dense(values).squeeze(-1) |
|
|
|
|
| def nse(prediction, target): |
| prediction, target = np.asarray(prediction), np.asarray(target) |
| denominator = np.sum((target - target.mean()) ** 2) |
| return float(1.0 - np.sum((prediction - target) ** 2) / max(denominator, 1e-12)) |
|
|
|
|
| def metrics(prediction, target): |
| prediction = np.asarray(prediction, dtype=np.float64) |
| target = np.asarray(target, dtype=np.float64) |
| correlation = float(np.corrcoef(prediction, target)[0, 1]) if prediction.size > 1 else 0.0 |
| if not np.isfinite(correlation): |
| correlation = 0.0 |
| alpha = float(prediction.std() / max(target.std(), 1e-12)) |
| beta = float(prediction.mean() / max(target.mean(), 1e-12)) |
| kge = float(1.0 - np.sqrt((correlation - 1) ** 2 + (alpha - 1) ** 2 + (beta - 1) ** 2)) |
| return { |
| "kge": kge, |
| "kge_r": correlation, |
| "kge_alpha": alpha, |
| "kge_beta": beta, |
| "nse": nse(prediction, target), |
| "rmse_m3_s": float(np.sqrt(np.mean((prediction - target) ** 2))), |
| } |
|
|
|
|
| def load_member(payload, device): |
| model = StreamflowLSTM(hidden_size=payload["hidden_size"], dropout=payload["dropout"]).to(device) |
| model.load_state_dict(payload["state_dict"]) |
| model.eval() |
| return model, payload |
|
|