Publish CLM5-Emulator reproduction
Browse files- .gitattributes +7 -35
- conf/config.yaml +31 -0
- config.json +1 -0
- model/clm5_emulator.py +24 -0
- scripts/fake_data.py +5 -0
- scripts/inference.py +7 -0
- scripts/result.py +8 -0
- scripts/train.py +16 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,7 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.py text eol=lf
|
| 2 |
+
*.md text eol=lf
|
| 3 |
+
*.json text eol=lf
|
| 4 |
+
*.yaml text eol=lf
|
| 5 |
+
*.pt binary
|
| 6 |
+
*.npz binary
|
| 7 |
+
*.png binary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
format_version: clm5_emulator_v1
|
| 4 |
+
path: data/clm5_ppe.npz
|
| 5 |
+
samples: 40
|
| 6 |
+
grid: [46, 72]
|
| 7 |
+
input_parameters: 6
|
| 8 |
+
targets: [GPP, LHF]
|
| 9 |
+
eof_modes: 3
|
| 10 |
+
model:
|
| 11 |
+
hidden_dim: 12
|
| 12 |
+
train:
|
| 13 |
+
epochs: 4
|
| 14 |
+
batch_size: 10
|
| 15 |
+
learning_rate: 0.005
|
| 16 |
+
weight_decay: 0.0001
|
| 17 |
+
paths:
|
| 18 |
+
checkpoint: result/checkpoints/clm5_emulator.pt
|
| 19 |
+
training_metrics: result/training/metrics.json
|
| 20 |
+
predictions: result/output/predictions.npz
|
| 21 |
+
evaluation: result/evaluation/metrics.json
|
| 22 |
+
figure: result/evaluation/comparison.png
|
| 23 |
+
paper_model:
|
| 24 |
+
ppe_members: 100
|
| 25 |
+
input_parameters: [medlynslope, dleaf, kmax, fff, dint, baseflow_scalar]
|
| 26 |
+
hidden_layers: 2
|
| 27 |
+
hidden_candidates: [5, 15]
|
| 28 |
+
optimizer: RMSprop
|
| 29 |
+
learning_rate: 0.005
|
| 30 |
+
batch_size: 30
|
| 31 |
+
split: [0.6, 0.2, 0.2]
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"model_name":"CLM5-Emulator","model_type":"clm5_emulator","architectures":["CLM5Emulator"],"framework":"PyTorch","domain":"land-surface","task":"emulation-and-parameter-estimation","implementation":{"entry_point":"model/clm5_emulator.py","scope":"L2 core-method engineering reproduction"},"architecture":{"input_parameters":6,"independent_targets":["GPP","LHF"],"eof_modes_per_target":3,"hidden_layers":2,"activations":["relu","tanh","linear"]},"data":{"driver":"GSWP3","forcing_interval_hours":3,"simulation_years":20,"spinup_years":15,"analysis_years":5,"paper_grid_degrees":[4,5],"engineering_grid_ledger":[46,72],"synthetic":true},"configuration_sources":["conf/config.yaml","model/clm5_emulator.py","scripts/fake_data.py","scripts/train.py","scripts/inference.py","scripts/result.py"]}
|
model/clm5_emulator.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CLM5 GPP/LHF EOF emulators and bounded parameter search."""
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
PARAMETERS=("medlynslope","dleaf","kmax","fff","dint","baseflow_scalar")
|
| 10 |
+
def load_config(root):return yaml.safe_load((Path(root)/"conf/config.yaml").read_text())
|
| 11 |
+
class TargetEmulator(nn.Module):
|
| 12 |
+
def __init__(self,hidden_dim=12):super().__init__();self.net=nn.Sequential(nn.Linear(6,hidden_dim),nn.ReLU(),nn.Linear(hidden_dim,hidden_dim),nn.Tanh(),nn.Linear(hidden_dim,3))
|
| 13 |
+
def forward(self,x):return self.net(x)
|
| 14 |
+
class CLM5Emulator(nn.Module):
|
| 15 |
+
def __init__(self,hidden_dim=12):super().__init__();self.gpp=TargetEmulator(hidden_dim);self.lhf=TargetEmulator(hidden_dim);self.model_config={"hidden_dim":hidden_dim}
|
| 16 |
+
def forward(self,x):
|
| 17 |
+
if x.ndim!=2 or x.shape[1]!=6:raise ValueError("expected [B,6] normalized parameters")
|
| 18 |
+
return torch.stack((self.gpp(x),self.lhf(x)),1)
|
| 19 |
+
def reconstruct(pc,eof,mean):return np.einsum("btm,tmg->btg",pc,eof)+mean[None]
|
| 20 |
+
def bounded_search(model,observed_pc,scales,weights,candidates=4096,seed=0):
|
| 21 |
+
rng=np.random.default_rng(seed);p=torch.from_numpy(rng.random((candidates,6)).astype("float32"));
|
| 22 |
+
with torch.no_grad():pred=model(p).numpy()
|
| 23 |
+
cost=(((pred-observed_pc[None])/scales[None])**2*weights[None]).sum((1,2));i=int(cost.argmin());return p[i].numpy(),float(cost[i])
|
| 24 |
+
def write_json(path,obj):path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);path.write_text(json.dumps(obj,indent=2)+"\n")
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,numpy as np
|
| 3 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 4 |
+
from model.clm5_emulator import *
|
| 5 |
+
c=load_config(ROOT);rng=np.random.default_rng(c["seed"]);n=c["data"]["samples"];p=rng.random((n,6)).astype("float32");lat=np.linspace(-90,90,46);lon=np.arange(72)*5;la,lo=np.meshgrid(lat,lon,indexing="ij");eof=np.stack([[np.cos(np.deg2rad(la)),np.sin(np.deg2rad(lo)),np.sin(np.deg2rad(la*2+lo))],[np.cos(np.deg2rad(la))*np.cos(np.deg2rad(lo)),np.sin(np.deg2rad(la)),np.cos(np.deg2rad(lo*2))]]).astype("float32");eof=eof.reshape(2,3,-1);pc=np.stack((np.stack((2*p[:,0]-p[:,3],p[:,1]*p[:,2],np.sin(np.pi*p[:,4])),1),np.stack((p[:,2]+p[:,5],p[:,0]*p[:,4],np.cos(np.pi*p[:,3])),1)),1).astype("float32");mean=np.stack((2+np.cos(np.deg2rad(la)),50+5*np.sin(np.deg2rad(la)))).reshape(2,-1).astype("float32");fields=reconstruct(pc,eof,mean).reshape(n,2,46,72);split=np.array([0]*24+[1]*8+[2]*8);path=ROOT/c["data"]["path"];path.parent.mkdir(parents=True,exist_ok=True);np.savez_compressed(path,parameters=p,pc=pc,eof=eof,mean=mean,fields=fields,split=split,latitude=lat,longitude=lon,format_version=c["data"]["format_version"]);print(path)
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,numpy as np,torch
|
| 3 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 4 |
+
from model.clm5_emulator import *
|
| 5 |
+
c=load_config(ROOT);d=np.load(ROOT/c["data"]["path"]);ck=torch.load(ROOT/c["paths"]["checkpoint"],map_location="cpu",weights_only=True);m=CLM5Emulator(**ck["model_config"]);m.load_state_dict(ck["model"]);mask=d["split"]==2
|
| 6 |
+
with torch.no_grad():pc=m(torch.tensor(d["parameters"][mask])).numpy()
|
| 7 |
+
fields=reconstruct(pc,d["eof"],d["mean"]).reshape(-1,2,46,72);obs=d["pc"].mean(0);best,cost=bounded_search(m,obs,np.maximum(d["pc"].std(0),.1),np.ones((2,3)),seed=c["seed"]);path=ROOT/c["paths"]["predictions"];path.parent.mkdir(parents=True,exist_ok=True);np.savez_compressed(path,predicted_pc=pc,target_pc=d["pc"][mask],predicted_fields=fields,target_fields=d["fields"][mask],best_parameters=best,cost=cost,latitude=d["latitude"],longitude=d["longitude"]);print(path)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,numpy as np
|
| 3 |
+
import matplotlib;matplotlib.use("Agg");import matplotlib.pyplot as plt
|
| 4 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 5 |
+
from model.clm5_emulator import load_config,write_json
|
| 6 |
+
c=load_config(ROOT);d=np.load(ROOT/c["paths"]["predictions"]);p=d["predicted_pc"];t=d["target_pc"];r2=[]
|
| 7 |
+
for v in range(2):r2.append([float(np.corrcoef(p[:,v,k],t[:,v,k])[0,1]**2) for k in range(3)])
|
| 8 |
+
rmse=np.sqrt(np.mean((d["predicted_fields"]-d["target_fields"])**2,axis=(0,2,3)));write_json(ROOT/c["paths"]["evaluation"],{"pc_r2":r2,"spatial_rmse":{"GPP":float(rmse[0]),"LHF":float(rmse[1])},"estimated_parameters":d["best_parameters"].tolist(),"calibration_cost":float(d["cost"]),"synthetic":True});fig,ax=plt.subplots(1,2,figsize=(9,3.5));ax[0].scatter(t[:,0,0],p[:,0,0]);ax[0].set(xlabel="CLM PC1",ylabel="Emulated PC1",title="GPP emulator");im=ax[1].imshow((d["predicted_fields"][0,1]-d["target_fields"][0,1]),cmap="coolwarm");ax[1].set_title("LHF spatial error");fig.colorbar(im,ax=ax[1]);fig.tight_layout();path=ROOT/c["paths"]["figure"];path.parent.mkdir(parents=True,exist_ok=True);fig.savefig(path,dpi=150);print(path)
|
scripts/train.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,os,torch,numpy as np
|
| 3 |
+
import torch.distributed as dist
|
| 4 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 5 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 6 |
+
from model.clm5_emulator import *
|
| 7 |
+
c=load_config(ROOT);rank=int(os.getenv("RANK",0));world=int(os.getenv("WORLD_SIZE",1));distributed=world>1
|
| 8 |
+
if distributed:dist.init_process_group("gloo")
|
| 9 |
+
torch.manual_seed(c["seed"]);d=np.load(ROOT/c["data"]["path"]);x=torch.tensor(d["parameters"][d["split"]==0]);y=torch.tensor(d["pc"][d["split"]==0]);base=CLM5Emulator(**c["model"]);m=DDP(base) if distributed else base;opt=torch.optim.RMSprop(m.parameters(),lr=c["train"]["learning_rate"],weight_decay=c["train"]["weight_decay"]);losses=[]
|
| 10 |
+
for _ in range(c["train"]["epochs"]):
|
| 11 |
+
for i in range(rank,len(x),world):loss=((m(x[i:i+1])-y[i:i+1])**2).mean();opt.zero_grad();loss.backward();opt.step();losses.append(float(loss))
|
| 12 |
+
v=torch.tensor([sum(losses),len(losses)],dtype=torch.float64)
|
| 13 |
+
if distributed:dist.all_reduce(v)
|
| 14 |
+
path=ROOT/c["paths"]["checkpoint"]
|
| 15 |
+
if rank==0:path.parent.mkdir(parents=True,exist_ok=True);torch.save({"model":base.state_dict(),"model_config":c["model"]},path);write_json(ROOT/c["paths"]["training_metrics"],{"mse":float(v[0]/v[1]),"world_size":world});print(path)
|
| 16 |
+
if distributed:dist.destroy_process_group()
|
weight/.gitkeep
ADDED
|
File without changes
|