| <!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 × <b>LEAKO</b> (LinearRoute.cpp:82-84) |
| interflow: leak = S × <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 → <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−误差平方和/观测方差:1 完美、0 等于"不如直接报均值"。对洪峰敏感。这是课程第一个端到端分数;模块11 正式展开。</div></div></details> |
| </div> |
|
|
| <div class="next"><a href="m05_runoff.html">← 5 产流</a><a href="index.html">导航 →</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> |
|
|