hydrologic-model-builder / m06_response.html
skyan1002's picture
Module 6 (response reservoirs): 4 editable versions, live NSE vs obs, log-scale baseflow view, pulse-response IUH panel
300744a verified
Raw
History Blame Contribute Delete
12.5 kB
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>模块6 · Response 响应水库</title>
<link rel="stylesheet" href="style.css"/>
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
<script src="data.js"></script>
</head>
<body>
<div class="wrap">
<h1>模块 6 · Response 响应水库(快/慢记忆)</h1>
<p class="sub">模块5 的 rs/ri 只是"分进快/慢袋子的水量",还不是流量。响应水库给它们<b>记忆</b>:延迟、削峰、退水。
慢库让河在两场雨之间还有水(基流行为)。<b>本页首次得到可与观测直接比的 Q(t) —— 课程第一个 NSE。</b></p>
<h2>对应 CREST/EF5 源码</h2>
<div class="card">
<p><code>LinearRoute.cpp</code> 每个格子恰好两个线性水库(这就是 V2 的出处):</p>
<div class="pipe">overland: leak = S &#215; <b>LEAKO</b> (LinearRoute.cpp:82-84)
interflow: leak = S &#215; <b>LEAKI</b> (LinearRoute.cpp:94-97)
河道格快流直通(:89-91);漏出的水再 cell-to-cell 传播(:102-140, 模块7/8)</div>
<p class="status">HBV 的 Kf/Ks 与之同构(课程 HBVMod.py)。V2 就是"行业标准件"。</p>
</div>
<h2>交互实验台:4 版本(可改代码 + 拖参数,实时 NSE)</h2>
<div class="card">
<table>
<tr><th>版本</th><th>思想</th><th>谁用</th><th>已知结果(CREST产流, 未率定/手调)</th></tr>
<tr><td>V1 single</td><td>一个库装全部</td><td>教学基线</td><td>NSE 0.40;枯季崩到 0 (log图死穴)</td></tr>
<tr><td><b>V2 dual</b></td><td>快库+慢库</td><td><b>CREST(LEAKO/LEAKI)</b> <span class="crest-tag">CREST</span>、HBV(Kf/Ks)</td><td>默认 0.20 &#8594; <b>手调 kf=0.15,ks=0.02 得 0.84</b></td></tr>
<tr><td>V3 nonlinear</td><td>快库 Q=kS^m</td><td>退水分析/变体</td><td>默认 -0.56(m 放大洪峰, 需配小 k)</td></tr>
<tr><td>V4 Nash cascade</td><td>n 库串联</td><td>Nash IUH(1957)/HYMOD</td><td>0.58;脉冲响应=Gamma形IUH(通往模块7)</td></tr>
</table>
<div id="tabs"></div>
<textarea id="code" class="code" spellcheck="false" style="min-height:250px"></textarea>
<div class="row" style="margin-top:10px">
<label id="s1lab">? = <span id="s1v"></span></label><input id="s1" type="range"/>
<label id="s2lab">? = <span id="s2v"></span></label><input id="s2" type="range"/>
<button id="run">运行 Run</button>
<button id="reset" class="ghost">重置代码</button>
<span id="status" class="status"></span>
</div>
<div id="plot1" class="plot" style="height:300px;margin-top:10px"></div>
<div class="cap">端到端流量 vs 观测(CREST 产流固定,只换 response;暖机 113 天已去除)</div>
<div id="plot2" class="plot" style="height:250px;margin-top:8px"></div>
<div class="cap">左:同一数据的 log 纵轴(枯季/基流照妖镜) · 右:当前配置的脉冲响应(你的 IUH)</div>
<div id="readout" class="readout"></div>
<div id="err"></div>
</div>
<h2>术语 Glossary</h2>
<div class="card">
<details class="gl" open><summary>响应水库 · Response reservoir / 记忆 memory</summary><div class="body">
<div class="en">A store that receives runoff volumes and releases them over time (Q=k·S). It converts instantaneous runoff "bags" into a flow time series; k sets the memory (half-life ln2/k).</div>
<div class="zh">接收产流量、按 Q=k·S 缓慢放水的水库——把"袋子"变成流量序列;k 定记忆长短(半衰期 ln2/k)。</div></div></details>
<details class="gl"><summary>LEAKO / LEAKI(EF5 汇流参数)</summary><div class="body">
<div class="en">Per-step leak fractions (0-1) of the overland and interflow reservoirs in EF5's linear-reservoir routing. Our V2 k_fast/k_slow map to them one-to-one.</div>
<div class="zh">EF5 线性水库汇流里 overland/interflow 库每步漏出的比例(0-1)。V2 的 k_fast/k_slow 与之一一对应。LEAKI 是枯季命脉:太大河就"断流"。</div></div></details>
<details class="gl"><summary>Nash IUH · 水库串联=单位线</summary><div class="body">
<div class="en">n equal linear stores in series: the pulse response is a Gamma distribution (Nash 1957). n=1 pure exponential; larger n gives a delayed bell — reservoirs ARE parameterized unit hydrographs (bridge to Module 7).</div>
<div class="zh">n 个相同线性库串联,脉冲响应是 Gamma 分布:n=1 纯指数,n 大则延迟成钟形——水库链就是参数化单位线(模块7 的桥)。在 V4 里拖 n 亲眼看。</div></div></details>
<details class="gl"><summary>NSE · Nash-Sutcliffe efficiency(首次登场)</summary><div class="body">
<div class="en">NSE = 1 - SSE/var(obs): 1 is perfect, 0 means "no better than the mean". Sensitive to peaks. Our first end-to-end score; formal treatment in Module 11.</div>
<div class="zh">NSE=1&#8722;误差平方和/观测方差:1 完美、0 等于"不如直接报均值"。对洪峰敏感。这是课程第一个端到端分数;模块11 正式展开。</div></div></details>
</div>
<div class="next"><a href="m05_runoff.html">&#8592; 5 产流</a><a href="index.html">导航 &#8594;</a></div>
<div class="foot">data: TU Delft CIE4431 Hesperange · anchor: CREST/EF5 LinearRoute.cpp</div>
</div>
<script src="shared.js"></script>
<script>
renderNav("m06_response.html");
const SNIPPETS={
single:`# V1 single linear reservoir -- everything in one store
def simulate(RS, RI, k, unused):
n=len(RS); Q=np.zeros(n); Qf=np.zeros(n); S=0.0
for t in range(n):
S += RS[t] + RI[t]
q = k*S; S -= q
Q[t]=q; Qf[t]=q
return Q, Qf, Q*0`,
dual:`# V2 dual linear reservoirs -- CREST LEAKO/LEAKI (LinearRoute.cpp:82-97), HBV Kf/Ks
def simulate(RS, RI, k_fast, k_slow):
n=len(RS); Q=np.zeros(n); Qf=np.zeros(n); Qs=np.zeros(n)
Sf=0.0; Ss=0.0
for t in range(n):
Sf += RS[t]
qf = k_fast*Sf; Sf -= qf # overlandLeak (cpp:82)
Ss += RI[t]
qs = k_slow*Ss; Ss -= qs # interflowLeak (cpp:95)
Q[t]=qf+qs; Qf[t]=qf; Qs[t]=qs
return Q, Qf, Qs`,
nonlinear:`# V3 nonlinear fast store Q=k*S**m (+ linear slow, ks=0.02)
def simulate(RS, RI, k_fast, m):
ks=0.02
n=len(RS); Q=np.zeros(n); Qf=np.zeros(n); Qs=np.zeros(n)
Sf=0.0; Ss=0.0
for t in range(n):
Sf += RS[t]
qf = min(k_fast*max(Sf,0.0)**m, Sf); Sf -= qf
Ss += RI[t]
qs = ks*Ss; Ss -= qs
Q[t]=qf+qs; Qf[t]=qf; Qs[t]=qs
return Q, Qf, Qs`,
nash:`# V4 Nash cascade -- n equal stores in series; pulse response = Gamma IUH (Nash 1957)
def simulate(RS, RI, n_stores, k):
n=len(RS); Q=np.zeros(n); S=[0.0]*int(round(n_stores))
for t in range(n):
inflow = RS[t] + RI[t]
for i in range(len(S)): # store i drains into store i+1
S[i] += inflow
inflow = k*S[i]
S[i] -= inflow
Q[t] = inflow
return Q, Q.copy(), Q*0`};
const SLIDERS={
single: {s1:{lab:"k",min:0.01,max:0.95,step:0.01,val:0.2,fmt:v=>v.toFixed(2)},
s2:{lab:"(unused)",min:0,max:1,step:1,val:0,fmt:v=>""}},
dual: {s1:{lab:"k_fast (LEAKO)",min:0.02,max:0.95,step:0.01,val:0.15,fmt:v=>v.toFixed(2)},
s2:{lab:"k_slow (LEAKI)",min:0.002,max:0.2,step:0.002,val:0.02,fmt:v=>v.toFixed(3)}},
nonlinear:{s1:{lab:"k_fast",min:0.005,max:0.9,step:0.005,val:0.05,fmt:v=>v.toFixed(3)},
s2:{lab:"m 指数",min:1.0,max:3.0,step:0.1,val:1.5,fmt:v=>v.toFixed(1)}},
nash: {s1:{lab:"n 库数",min:1,max:7,step:1,val:3,fmt:v=>v.toFixed(0)},
s2:{lab:"k",min:0.02,max:0.95,step:0.01,val:0.4,fmt:v=>v.toFixed(2)}}};
let currentKey="dual";
const codeEl=document.getElementById("code");
codeEl.value=SNIPPETS.dual;
let runFn=null;
function setSliders(key){
for(const s of ["s1","s2"]){
const cfg=SLIDERS[key][s], el=document.getElementById(s);
el.min=cfg.min; el.max=cfg.max; el.step=cfg.step; el.value=cfg.val;
document.getElementById(s+"lab").firstChild.textContent=cfg.lab+" = ";
document.getElementById(s+"v").textContent=cfg.fmt(cfg.val);
}
}
makeTabs("tabs",[
{key:"single",label:"V1 single"},
{key:"dual",label:"V2 dual",crest:true},
{key:"nonlinear",label:"V3 nonlinear"},
{key:"nash",label:"V4 Nash cascade"},
],k=>{currentKey=k; codeEl.value=SNIPPETS[k]; setSliders(k); if(runFn)runFn();});
for(const b of document.getElementById("tabs").children) b.className=(b.dataset.key==="dual")?"on":"";
setSliders("dual");
for(const s of ["s1","s2"]){
const el=document.getElementById(s);
el.addEventListener("input",()=>{document.getElementById(s+"v").textContent=SLIDERS[currentKey][s].fmt(parseFloat(el.value));});
el.addEventListener("change",()=>runFn&&runFn());
}
runFn=makeRunner({
codeEl, runBtn:document.getElementById("run"), resetBtn:document.getElementById("reset"),
statusEl:document.getElementById("status"), errEl:document.getElementById("err"),
params:()=>({s1:parseFloat(document.getElementById("s1").value),
s2:parseFloat(document.getElementById("s2").value)}),
buildScript:(code,p)=>`
import numpy as np
P = np.array(${JSON.stringify(PRECIP)})
PETIN = np.array(${JSON.stringify(PET)})
QOBS = np.array(${JSON.stringify(QOBS.map(v=>v==null?NaN:v))})
# ---- fixed upstream chain: GivenPET(KE=1) + CREST soil (faithful, defaults) ----
def crest_step(Pt, pet, W, WM=120.0, B=1.0, IM=0.03, FC=6.0):
if Pt > pet:
Psoil=(Pt-pet)*(1-IM); Pimp=Pt-pet-Psoil
W=min(W,WM); Wmaxm=WM*(1+B)
A=Wmaxm*(1-(1-W/WM)**(1/(1+B)))
if Psoil+A>=Wmaxm: R=max(Psoil-(WM-W),0.0); Wo=WM
else:
infil=WM*((1-A/Wmaxm)**(1+B)-(1-(A+Psoil)/Wmaxm)**(1+B))
infil=min(infil,Psoil); R=max(Psoil-infil,0.0); Wo=W+infil
temX=(W+Wo)/WM/2*FC
ri=R if R<=temX else temX
return Wo, R-ri+Pimp, ri
else:
take=min((pet-Pt)*W/120.0, W)
return W-take, 0.0, 0.0
n=len(P); RS=np.zeros(n); RI=np.zeros(n); W=60.0
for t in range(n):
W, rs, ri = crest_step(float(P[t]), float(PETIN[t]), W)
RS[t]=rs; RI[t]=ri
# ---- your response code ----
${code}
Q, Qf, Qs = simulate(RS, RI, ${p.s1}, ${p.s2})
Q=np.asarray(Q,float)
WARM=113
o=QOBS[WARM:]; s=Q[WARM:]; msk=~np.isnan(o)
nse = 1 - np.sum((s[msk]-o[msk])**2)/np.sum((o[msk]-o[msk].mean())**2)
fastpct = float(np.sum(Qf)/max(np.sum(Q),1e-9)*100)
# pulse response (your IUH)
pu=np.zeros(60); pu[2]=10.0
Qp,_,_ = simulate(pu, np.zeros(60), ${p.s1}, ${p.s2})
__out={"q":Q[WARM:700].tolist(),"obs":np.where(np.isnan(QOBS),None,QOBS)[WARM:700].tolist(),
"nse":float(nse),"fast":fastpct,"pulse":np.asarray(Qp,float).tolist()}
`,
onResult:(r,p,ms)=>{
const x=[...Array(r.q.length).keys()];
Plotly.react("plot1",[
{x:x,y:r.obs,type:"scatter",mode:"lines",name:"Q_obs",line:{color:"#0f172a",width:1.3}},
{x:x,y:r.q,type:"scatter",mode:"lines",name:"simulated",line:{color:"#16a34a",width:1.3}},
],{margin:{t:8,r:10,b:38,l:52},legend:{orientation:"h",y:1.15},
xaxis:{title:"day since warm-up"},yaxis:{title:"Q (mm/d)"}},{responsive:true,displayModeBar:false});
const px=[...Array(r.pulse.length).keys()];
Plotly.react("plot2",[
{x:x,y:r.obs.map(v=>v==null?null:Math.max(v,1e-3)),type:"scatter",mode:"lines",name:"Q_obs (log)",line:{color:"#0f172a",width:1},yaxis:"y"},
{x:x,y:r.q.map(v=>Math.max(v,1e-3)),type:"scatter",mode:"lines",name:"sim (log)",line:{color:"#16a34a",width:1},yaxis:"y"},
{x:px,y:r.pulse,type:"bar",name:"pulse response (IUH)",marker:{color:"#7c3aed"},xaxis:"x2",yaxis:"y2"},
],{margin:{t:8,r:10,b:38,l:52},showlegend:false,
grid:{rows:1,columns:2,pattern:"independent"},
xaxis:{title:"day (log Q)"},yaxis:{type:"log",range:[-3,1.4]},
xaxis2:{title:"step (10mm pulse)"},yaxis2:{title:"IUH"}},{responsive:true,displayModeBar:false});
document.getElementById("readout").innerHTML=
`<b>NSE = ${r.nse.toFixed(3)}</b>(1完美, 0=不如报均值; 手调最好 0.84)| 快流占比=${r.fast.toFixed(0)}% | 运行 ${ms.toFixed(0)} ms<br>`+
`试试:V2 拖 k_slow 看 log 图枯季尾巴(LEAKI=枯季命脉);V4 拖 n 看 IUH 从指数长成钟形(Nash)。`;
}
});
</script>
</body>
</html>