zhangrenchao commited on
Commit
835fa51
·
verified ·
1 Parent(s): ae2d9f8

Publish CLM5-Emulator reproduction

Browse files
.gitattributes CHANGED
@@ -1,35 +1,7 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
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