hydrologic-model-builder / m01_forcing.html
skyan1002's picture
Restructure: multi-page interactive course (nav home + per-module pages with editable/runnable code, version tabs w/ CREST tag, live plots)
8f8a793 verified
Raw
History Blame Contribute Delete
9.43 kB
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>模块1 · Forcing 驱动数据</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>模块 1 · Forcing(驱动数据)</h1>
<p class="sub">forcing = 逐时间步喂给模型的气象输入(降水/PET/温度)。模型再好,输入不对必错(garbage in, garbage out)。</p>
<h2>真实数据:Hesperange 三要素</h2>
<div class="card">
<p class="sub">拖动下面窗口滑块看不同时段。注意 PET 的季节循环、流量冬春涨夏季退。</p>
<div class="row">
<label>起始天 <span id="d0v">0</span></label><input id="d0" type="range" min="0" max="1125" step="1" value="0"/>
<label>结束天 <span id="d1v">1126</span></label><input id="d1" type="range" min="1" max="1126" step="1" value="1126"/>
</div>
<div id="forc" class="plot" style="height:400px"></div>
</div>
<h2>方法菜单:空间聚合(点雨量站 &#8594; 流域面平均)— 可切换 &amp; 可改代码</h2>
<div class="card">
<table>
<tr><th>版本</th><th>思想</th><th>谁用</th><th>可微性</th></tr>
<tr><td>arithmetic mean</td><td>算术平均</td><td>简单集总</td><td>线性&#8594;可微</td></tr>
<tr><td><b>Thiessen (1911)</b></td><td>面积加权(最近邻多边形)</td><td><b>HBV 等经典集总</b></td><td>线性&#8594;可微</td></tr>
<tr><td>IDW</td><td>反距离加权 1/d^p</td><td>插值/半分布预处理</td><td>线性&#8594;可微</td></tr>
<tr><td>gridded 网格</td><td>雷达MRMS/卫星GPM 逐格、不面平均</td><td><b>CREST/EF5</b> <span class="crest-tag">CREST</span></td><td>模块8(分布式)</td></tr>
</table>
<p class="status">注意:CREST 的选择(gridded)属于分布式阶段,本页先对比三种"面平均"法。Hesperange 数据已是面平均,所以用<b>合成 3 站</b>(对真实序列加偏差+噪声)演示——合成部分已在代码里标明。</p>
<div id="tabs"></div>
<textarea id="code" class="code" spellcheck="false"></textarea>
<div class="row" style="margin-top:10px">
<label>IDW 幂次 p = <span id="pv">2.0</span></label>
<input id="pw" type="range" min="0.5" max="4.0" step="0.1" value="2.0"/>
<label>站1权重因子(Thiessen面积) <span id="a1v">120</span> km²</label>
<input id="a1" type="range" min="20" max="220" step="5" value="120"/>
<button id="run">运行 Run</button>
<button id="reset" class="ghost">重置代码</button>
<span id="status" class="status"></span>
</div>
<div id="plot" class="plot" style="height:330px;margin-top:10px"></div>
<div id="readout" class="readout"></div>
<div id="err"></div>
</div>
<h2>术语 Glossary</h2>
<div class="card">
<details class="gl" open><summary>流域面平均降水 · Catchment-average precipitation</summary><div class="body">
<div class="en">Aggregating point gauges into one basin-representative series — the standard input of lumped models.</div>
<div class="zh">把点雨量站汇成一个代表全流域的降水序列,集总模型的标准输入。</div></div></details>
<details class="gl"><summary>Thiessen 多边形 · Thiessen polygon (1911)</summary><div class="body">
<div class="en">Weight each gauge by the area for which it is the nearest gauge (its polygon).</div>
<div class="zh">按"每站最近邻控制的面积"加权求面平均;经典、简单、常用于业务集总模型。</div></div></details>
<details class="gl"><summary>IDW · Inverse-distance weighting</summary><div class="body">
<div class="en">Weights ∝ 1/distance^p to a target point (e.g. centroid); p controls how fast influence decays.</div>
<div class="zh">权重 ∝ 1/距离^p;p 越大近站影响越强。上面的滑块就是 p。</div></div></details>
<details class="gl"><summary>网格降水 · Gridded precipitation (CREST 的选择)</summary><div class="body">
<div class="en">Per-cell precipitation fields from radar (MRMS) or satellite (TRMM/GPM); CREST/EF5 ingests these directly per grid cell — no averaging.</div>
<div class="zh">雷达/卫星反演的逐网格降水场;CREST/EF5 逐格直接吃、不做面平均(见模块8 分布式)。</div></div></details>
<details class="gl"><summary>率定/验证划分 · Calibration / Validation split &amp; 暖机 Warm-up</summary><div class="body">
<div class="en">Tune on one period, test on an independent one; discard an initial warm-up while states converge.</div>
<div class="zh">一段调参、另一段独立检验防过拟合;开头暖机期让状态收敛、评估时丢弃。本课划分:113/676/338 天。</div></div></details>
</div>
<div class="next"><a href="m00_overview.html">&#8592; 0 水量平衡</a><a href="m02_pet.html">下一模块:2 PET &#8594;</a></div>
<div class="foot">data: TU Delft CIE4431 Hesperange · anchor: CREST/EF5</div>
</div>
<script src="shared.js"></script>
<script>
renderNav("m01_forcing.html");
const DAYS=[...Array(PRECIP.length).keys()];
/* forcing viewer */
function drawForcing(){
const a=parseInt(document.getElementById("d0").value), b=Math.max(a+1,parseInt(document.getElementById("d1").value));
const xs=DAYS.slice(a,b);
Plotly.react("forc",[
{x:xs,y:PRECIP.slice(a,b),type:"bar",name:"precip",marker:{color:"#2563eb"},yaxis:"y"},
{x:xs,y:PET.slice(a,b),type:"scatter",mode:"lines",name:"PET",line:{color:"#ea580c",width:1},yaxis:"y2"},
{x:xs,y:QOBS.slice(a,b),type:"scatter",mode:"lines",name:"Q_obs",line:{color:"#16a34a",width:1.2},yaxis:"y3"},
],{margin:{t:8,r:10,b:38,l:55},showlegend:false,
xaxis:{anchor:"y3",title:"day"},
yaxis:{domain:[0.7,1],title:"P",autorange:"reversed"},
yaxis2:{domain:[0.36,0.66],title:"PET"},
yaxis3:{domain:[0,0.32],title:"Q_obs"}},{responsive:true,displayModeBar:false});
}
bindSlider("d0","d0v",v=>v.toFixed(0),drawForcing);
bindSlider("d1","d1v",v=>v.toFixed(0),drawForcing);
drawForcing();
/* aggregation versions: each tab loads an editable python snippet */
const SNIPPETS={
mean:`# V1 arithmetic mean -- USED_BY: simple lumped setups
def aggregate(gauges, areas, dists, p):
"""gauges: (T,3) synthetic stations. Return catchment-average (T,)."""
return np.nanmean(gauges, axis=1)`,
thiessen:`# V2 Thiessen (1911) area-weighted -- USED_BY: HBV-era lumped (classic choice)
def aggregate(gauges, areas, dists, p):
w = areas / areas.sum() # polygon-area weights
return np.nansum(gauges * w[None, :], axis=1)`,
idw:`# V3 inverse-distance weighting -- USED_BY: interpolation / semi-distributed pre-processing
def aggregate(gauges, areas, dists, p):
w = 1.0 / dists**p # closer gauge -> larger weight
w = w / w.sum()
return np.nansum(gauges * w[None, :], axis=1)`};
const codeEl=document.getElementById("code");
codeEl.value=SNIPPETS.thiessen; // start on the classic
let runFn=null;
const getTab=makeTabs("tabs",[
{key:"mean",label:"V1 mean"},
{key:"thiessen",label:"V2 Thiessen (HBV)"},
{key:"idw",label:"V3 IDW"},
],k=>{codeEl.value=SNIPPETS[k]; if(runFn)runFn();});
bindSlider("pw","pv",v=>v.toFixed(1),()=>runFn&&runFn());
bindSlider("a1","a1v",v=>v.toFixed(0),()=>runFn&&runFn());
runFn=makeRunner({
codeEl, runBtn:document.getElementById("run"), resetBtn:document.getElementById("reset"),
statusEl:document.getElementById("status"), errEl:document.getElementById("err"),
params:()=>({p:parseFloat(document.getElementById("pw").value),
a1:parseFloat(document.getElementById("a1").value)}),
buildScript:(code,prm)=>`
import numpy as np
P = np.array(${JSON.stringify(PRECIP)})
rng = np.random.default_rng(0)
# SYNTHETIC 3 gauges: bias factors x noise around the true catchment-average
factors = np.array([1.15, 0.9, 0.95])
gauges = np.clip(P[:,None]*factors[None,:]*(1+0.1*rng.standard_normal((len(P),3))), 0, None)
areas = np.array([${prm.a1}, 90.0, 82.0]) # Thiessen polygon areas (km2) - slider controls station 1
dists = np.array([3.0, 5.0, 8.0]) # distance to centroid (km)
${code}
avg = np.asarray(aggregate(gauges, areas, dists, ${prm.p}), float)
yrs = len(P)/365.25
__out = {"avg": avg.tolist(), "annual": float(avg.sum()/yrs), "true_annual": float(P.sum()/yrs)}
`,
onResult:(r,prm,ms)=>{
const a=150,b=260, xs=DAYS.slice(a,b);
linePlot("plot",[
{x:xs,y:r.avg.slice(a,b),type:"scatter",mode:"lines",name:"aggregated",line:{color:"#2563eb",width:1.4}},
{x:xs,y:PRECIP.slice(a,b),type:"scatter",mode:"lines",name:"true catchment-avg",line:{color:"#0f172a",width:1.2,dash:"dash"}},
],"precip (mm/d)","day (zoom 150-260)");
document.getElementById("readout").innerHTML=
`年降水=<b>${r.annual.toFixed(0)} mm/yr</b>(真值 ${r.true_annual.toFixed(0)}) | `+
`偏差 <b>${(100*(r.annual-r.true_annual)/r.true_annual).toFixed(1)}%</b> | 运行 <b>${ms.toFixed(0)} ms</b><br>`+
`试试:切换版本看年降水如何变化;拖站1面积/IDW幂次;或直接改代码里的权重公式。`;
}
});
</script>
</body>
</html>