初始化协作仓库:赛题材料、初步试解与协作说明
- 大文件(附件原始包、data/)由 files.ddwsgood.top 承载,不入库 - 新增 .gitignore/.gitattributes/COLLAB.md
This commit is contained in:
1 parent
d87c3b54eb
commit
ac3e481e11
75 files changed
+2389
No files matched your search
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
全球冰川灾害编目(TPDC 3ecd6ef8,DOI 10.11888/Cryos.tpdc.272812)审计与试算 —— 修正版 v2。
|
||||
AI 声明:DeepSeek-V4.1-Flash(DeepSeek),2026-10-23。需人工复核。
|
||||
|
||||
v2 修正(对应主线程审查意见):
|
||||
1 去重键含完整规范化日期/区间;仅到年的记录只标“疑似重复”,不删除;统一称“候选去重”。
|
||||
2 时间解析顺序:区间 → 完整日期 → 年月 → 年份;非完整日期不记 day;保留原文。
|
||||
3 有效事件行 = 有 ID 或 Site 的真实数据行;缺日期不丢,单独标 missing;合并单元格只在真实合并范围内还原,不做无条件 ffill。
|
||||
4 audit 分 raw / candidate_unique 两套口径输出。
|
||||
5 同时保留 Trigger 与 Triggers 两个字段。
|
||||
|
||||
输出(覆盖前已在 backups/YYYYMMDD 备份):
|
||||
结果/catalog_slope_failures_clean.csv
|
||||
结果/catalog_glof_clean.csv
|
||||
结果/catalog_audit.json
|
||||
结果/runout_angle_stats.json
|
||||
"""
|
||||
import os, io, re, json, zipfile, datetime as dt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import openpyxl
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ZIP = os.path.abspath(os.path.join(ROOT, "..", "TPDC_补充数据", "下载",
|
||||
"3ecd6ef8-fb24-44f0-b9f1-c8839fe83760.zip"))
|
||||
RES = os.path.join(ROOT, "结果")
|
||||
os.makedirs(RES, exist_ok=True)
|
||||
RNG = np.random.default_rng(20261023) # 固定留出,保证可复现
|
||||
|
||||
z = zipfile.ZipFile(ZIP)
|
||||
names = [n for n in z.namelist() if n.endswith(".xlsx")]
|
||||
F_SF = [n for n in names if "slope failures" in n][0]
|
||||
F_GL = [n for n in names if "Moraine" in n][0]
|
||||
|
||||
def load_sheet(path, sheet):
|
||||
"""用 openpyxl 读;仅对真实 merged range 用左上角值回填,其余不 ffill。"""
|
||||
wb = openpyxl.load_workbook(io.BytesIO(z.read(path)), data_only=True)
|
||||
ws = wb[sheet]
|
||||
merged = {}
|
||||
for rng in ws.merged_cells.ranges:
|
||||
v = ws.cell(rng.min_row, rng.min_col).value
|
||||
for r in range(rng.min_row, rng.max_row + 1):
|
||||
for c in range(rng.min_col, rng.max_col + 1):
|
||||
merged[(r, c)] = v
|
||||
rows = list(ws.iter_rows(values_only=False))
|
||||
header = [c.value for c in rows[0]]
|
||||
recs, n_merged_filled = [], 0
|
||||
for r_i, row in enumerate(rows[1:], start=2):
|
||||
vals = []
|
||||
for c_i, cell in enumerate(row, start=1):
|
||||
v = cell.value
|
||||
if (v is None or v == "") and (r_i, c_i) in merged:
|
||||
v = merged[(r_i, c_i)]; n_merged_filled += 1
|
||||
vals.append(v)
|
||||
recs.append(vals)
|
||||
df = pd.DataFrame(recs, columns=[str(h).strip() if h is not None else f"col{i}" for i, h in enumerate(header)])
|
||||
df.attrs["merged_filled_cells"] = n_merged_filled
|
||||
df.attrs["merged_ranges"] = len(ws.merged_cells.ranges)
|
||||
return df
|
||||
|
||||
DATE_RE = re.compile(r"^\d{4}[-/]\d{1,2}[-/]\d{1,2}([ T]\d{1,2}:\d{2}(:\d{2})?)?$")
|
||||
YM_RE = re.compile(r"^\d{4}[-/]\d{1,2}$")
|
||||
Y_RE = re.compile(r"^\d{4}$")
|
||||
|
||||
def parse_time(v):
|
||||
"""返回 (原文, 精度, 起始年, 结束年, 规范化日期键)。
|
||||
精度:hour / day / month / year / range / other / missing。"""
|
||||
if v is None or (isinstance(v, float) and np.isnan(v)):
|
||||
return None, "missing", None, None, None
|
||||
if isinstance(v, (dt.datetime, pd.Timestamp)):
|
||||
s = v.strftime("%Y-%m-%d %H:%M:%S")
|
||||
prec = "hour" if (v.hour or v.minute or v.second) else "day"
|
||||
return s, prec, v.year, v.year, s[:10]
|
||||
s = str(v).strip()
|
||||
if s in ("", "None", "nan", "NaN"):
|
||||
return None, "missing", None, None, None
|
||||
# 1) 区间:出现两个 4 位年份(2014/2-2015/4、2000-2001、2014/2—2015/4)
|
||||
yrs = [int(t) for t in re.findall(r"(1[5-9]\d{2}|20\d{2})", s)]
|
||||
if len(yrs) >= 2:
|
||||
return s, "range", min(yrs), max(yrs), None
|
||||
# 2) 完整日期(可带时间)
|
||||
if DATE_RE.match(s):
|
||||
d = s.replace("/", "-")
|
||||
prec = "hour" if (" " in d or "T" in d) else "day"
|
||||
try:
|
||||
y = int(d[:4])
|
||||
except Exception:
|
||||
return s, "other", None, None, None
|
||||
return s, prec, y, y, d[:10]
|
||||
# 3) 年月
|
||||
if YM_RE.match(s):
|
||||
y = int(s[:4])
|
||||
return s, "month", y, y, None
|
||||
# 4) 年份
|
||||
if Y_RE.match(s):
|
||||
return s, "year", int(s), int(s), None
|
||||
# 5) 其他(含中文、英文月份、约等)
|
||||
return s, "other", (yrs[0] if yrs else None), (yrs[0] if yrs else None), None
|
||||
|
||||
SHEETS = ["Glacier detachments", "Ice avalanches", "Ice-rock avalanches",
|
||||
"Rock avalanches", "GLOF triggered by avalanches", "GLOF triggered by calving"]
|
||||
|
||||
rows, sheet_diag = [], {}
|
||||
for sh in SHEETS:
|
||||
df = load_sheet(F_SF, sh)
|
||||
n_raw = len(df)
|
||||
n_evt = n_time_missing = 0
|
||||
for _, r in df.iterrows():
|
||||
eid = r.get("ID"); site = r.get("Site")
|
||||
# 真实事件行:ID 或 Site 至少一个非空(整行空行不计)
|
||||
if (pd.isna(eid) and pd.isna(site)):
|
||||
continue
|
||||
n_evt += 1
|
||||
t_raw = r.get("Time")
|
||||
ts, prec, y0, y1, dkey = parse_time(t_raw)
|
||||
if prec == "missing":
|
||||
n_time_missing += 1
|
||||
try:
|
||||
lon = float(r.get("Longitude")); lat = float(r.get("Latitude"))
|
||||
except Exception:
|
||||
lon = lat = np.nan
|
||||
rows.append(dict(
|
||||
sheet=sh, src_ID=eid, Site=str(site).strip() if not pd.isna(site) else None,
|
||||
time_raw=ts, time_prec=prec, year_start=y0, year_end=y1, date_key=dkey,
|
||||
mechanism=r.get("Type"), lon=lon, lat=lat,
|
||||
angle_reach_deg=r.get("Angle of reach °"),
|
||||
source_area_km2=r.get("Source area (km2)"),
|
||||
volume_Mm3=r.get("Volume (106m3)"),
|
||||
source_slope_deg=r.get("Source slope (°)") if "Source slope (°)" in df.columns else r.get("Source slope °"),
|
||||
horizontal_km=r.get("Horizontal distance (km)"),
|
||||
affected_km2=r.get("Affected area (km2)"),
|
||||
cascade=r.get("Cascade hazards"),
|
||||
trigger_plural=r.get("Triggers"), # 多数表用 Triggers
|
||||
trigger_singular=r.get("Trigger"), # Ice-rock 表用 Trigger
|
||||
refs=r.get("References")))
|
||||
sheet_diag[sh] = dict(raw_rows=n_raw, event_rows=n_evt, time_missing=n_time_missing,
|
||||
merged_ranges=df.attrs["merged_ranges"],
|
||||
merged_cells_filled=df.attrs["merged_filled_cells"])
|
||||
|
||||
sf = pd.DataFrame(rows)
|
||||
for c in ["angle_reach_deg", "source_area_km2", "volume_Mm3", "source_slope_deg",
|
||||
"horizontal_km", "affected_km2"]:
|
||||
sf[c] = pd.to_numeric(sf[c], errors="coerce")
|
||||
sf["trigger"] = sf.trigger_plural.where(sf.trigger_plural.notna(), sf.trigger_singular)
|
||||
|
||||
def norm(s):
|
||||
return re.sub(r"[\s\-_]+", "", str(s).lower())
|
||||
|
||||
# ---- 去重:日期精确的才作为候选重复;年份精度的只标疑似
|
||||
sf["dedupe_key_exact"] = np.where(
|
||||
sf.date_key.notna(),
|
||||
sf.Site.map(norm) + "|" + sf.lat.round(3).astype(str) + "|" + sf.lon.round(3).astype(str) + "|" + sf.date_key.fillna(""),
|
||||
None)
|
||||
sf["dedupe_key_year"] = np.where(
|
||||
sf.date_key.isna() & sf.year_start.notna(),
|
||||
sf.Site.map(norm) + "|" + sf.lat.round(3).astype(str) + "|" + sf.lon.round(3).astype(str) + "|Y" + sf.year_start.fillna(0).astype(int).astype(str),
|
||||
None)
|
||||
sf["is_dupe_exact"] = sf.dedupe_key_exact.notna() & sf.duplicated("dedupe_key_exact", keep="first")
|
||||
sf["is_dupe_suspect"] = (~sf.is_dupe_exact) & sf.dedupe_key_year.notna() & sf.duplicated("dedupe_key_year", keep="first")
|
||||
sf["candidate_unique"] = ~(sf.is_dupe_exact | sf.is_dupe_suspect)
|
||||
|
||||
sf["is_hma"] = sf.lat.between(25, 45) & sf.lon.between(60, 105)
|
||||
sf["is_himalaya"] = sf.lat.between(26, 31) & sf.lon.between(75, 95)
|
||||
|
||||
# ---- GLOF 表
|
||||
gl_df = load_sheet(F_GL, "Sheet1")
|
||||
gl_rows = []
|
||||
for _, r in gl_df.iterrows():
|
||||
lake, date = r.get("Glacial lake"), r.get("Date")
|
||||
if pd.isna(lake) and pd.isna(date):
|
||||
continue
|
||||
ts, prec, y0, y1, dkey = parse_time(date)
|
||||
try:
|
||||
lon = float(r.get("Longitude")); lat = float(r.get("Latitude"))
|
||||
except Exception:
|
||||
lon = lat = np.nan
|
||||
gl_rows.append(dict(src_ID=r.get("ID"), country=r.get("Country"),
|
||||
lake=str(lake).strip() if not pd.isna(lake) else None,
|
||||
time_raw=ts, time_prec=prec, year_start=y0, year_end=y1, date_key=dkey,
|
||||
lake_type=r.get("Lake type"), lon=lon, lat=lat, triggers=r.get("Triggers"),
|
||||
volume_Mm3=pd.to_numeric(r.get("Release volume/106m3"), errors="coerce"),
|
||||
peak_discharge=r.get("Peak discharge/m3·s-1"), deaths=r.get("Deaths"),
|
||||
refs=r.get("References")))
|
||||
gl = pd.DataFrame(gl_rows)
|
||||
gl["is_hma"] = gl.lat.between(25, 45) & gl.lon.between(60, 105)
|
||||
gl["is_himalaya"] = gl.lat.between(26, 31) & gl.lon.between(75, 95)
|
||||
|
||||
def summarize(df, tag):
|
||||
raw = df
|
||||
uniq = df[df.candidate_unique] if "candidate_unique" in df else df
|
||||
return dict(
|
||||
raw=dict(n=int(len(raw)), by_sheet=raw.sheet.value_counts().to_dict() if "sheet" in raw else {},
|
||||
by_mechanism=raw.mechanism.astype(str).value_counts().to_dict() if "mechanism" in raw else {},
|
||||
by_precision=raw.time_prec.value_counts().to_dict(),
|
||||
hma=int(raw.is_hma.sum()), himalaya=int(raw.is_himalaya.sum())),
|
||||
candidate_unique=dict(n=int(len(uniq)),
|
||||
by_sheet=uniq.sheet.value_counts().to_dict() if "sheet" in uniq else {},
|
||||
by_mechanism=uniq.mechanism.astype(str).value_counts().to_dict() if "mechanism" in uniq else {},
|
||||
by_precision=uniq.time_prec.value_counts().to_dict(),
|
||||
hma=int(uniq.is_hma.sum()), himalaya=int(uniq.is_himalaya.sum())),
|
||||
duplicates=dict(exact_removed=int(df.is_dupe_exact.sum()) if "is_dupe_exact" in df else 0,
|
||||
suspect_year_only=int(df.is_dupe_suspect.sum()) if "is_dupe_suspect" in df else 0))
|
||||
|
||||
audit = dict(
|
||||
script_version="v2 (2026-10-23 correction)",
|
||||
source_zip=os.path.basename(ZIP), doi="10.11888/Cryos.tpdc.272812",
|
||||
metadata_claim=dict(slope_failures=737, glof=380),
|
||||
claim_note="737/380 是元数据描述;本表只报可复现的实际计数,不为对齐宣传数修改数据。",
|
||||
sheet_diagnostics=sheet_diag,
|
||||
slope_failures=summarize(sf, "slope"),
|
||||
slope_fields=dict(volume=int(sf.volume_Mm3.notna().sum()),
|
||||
volume_and_H=int((sf.volume_Mm3.notna() & sf.horizontal_km.notna()).sum()),
|
||||
angle=int(sf.angle_reach_deg.notna().sum())),
|
||||
glof=summarize(gl, "glof"),
|
||||
glof_fields=dict(volume=int(gl.volume_Mm3.notna().sum())),
|
||||
)
|
||||
|
||||
sf.to_csv(os.path.join(RES, "catalog_slope_failures_clean.csv"), index=False, encoding="utf-8-sig")
|
||||
gl.to_csv(os.path.join(RES, "catalog_glof_clean.csv"), index=False, encoding="utf-8-sig")
|
||||
with open(os.path.join(RES, "catalog_audit.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(audit, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("== sheet diagnostics")
|
||||
for k, v in sheet_diag.items():
|
||||
print(f" {k:<34} raw={v['raw_rows']:>4} events={v['event_rows']:>4} no_time={v['time_missing']:>4} "
|
||||
f"merged_ranges={v['merged_ranges']} merged_cells={v['merged_cells_filled']}")
|
||||
print("\n== slope failures")
|
||||
print(" raw:", audit["slope_failures"]["raw"])
|
||||
print(" cand-unique:", audit["slope_failures"]["candidate_unique"])
|
||||
print(" dup:", audit["slope_failures"]["duplicates"])
|
||||
print("\n== GLOF")
|
||||
print(" raw:", audit["glof"]["raw"])
|
||||
print(" cand-unique:", audit["glof"]["candidate_unique"])
|
||||
|
||||
# ---------------- 传播/摩擦(样本因去重口径变化,重算一次)
|
||||
print("\n" + "=" * 60)
|
||||
sub = sf[sf.candidate_unique & sf.volume_Mm3.gt(0) & sf.horizontal_km.gt(0)]
|
||||
print("体积+距离齐全(候选唯一):")
|
||||
print(sub.groupby("sheet").size().to_string())
|
||||
ice = sub[sub.sheet.isin(["Ice-rock avalanches", "Glacier detachments"])].copy()
|
||||
out = dict(script_version="v2", n_ice=len(ice), n_ice_hma=int(ice.is_hma.sum()))
|
||||
if len(ice) >= 12:
|
||||
x = np.log10(ice.volume_Mm3.values); y = np.log10(ice.horizontal_km.values)
|
||||
idx = RNG.permutation(len(ice)); cut = int(0.7*len(ice))
|
||||
tr, te = idx[:cut], idx[cut:]
|
||||
b, a = np.polyfit(x[tr], y[tr], 1)
|
||||
pred = a + b*x[te]
|
||||
rmse = float(np.sqrt(np.mean((pred - y[te])**2)))
|
||||
r2 = float(1 - np.sum((y[te]-pred)**2)/np.sum((y[te]-y[te].mean())**2))
|
||||
out.update(dict(fit=(float(a), float(b)), holdout_rmse_log10=rmse, holdout_r2=r2, n_test=int(len(te))))
|
||||
print(f"留出:log10H={a:.3f}+{b:.3f}log10V,RMSE={rmse:.2f},R²={r2:.2f}(n_test={len(te)})")
|
||||
else:
|
||||
out["note"] = "n<12, no fit"
|
||||
|
||||
ang = sf[sf.candidate_unique & sf.angle_reach_deg.between(0.5, 60)]["angle_reach_deg"]
|
||||
mu = np.tan(np.deg2rad(ang))
|
||||
out["angle_reach_deg"] = dict(n=int(len(ang)), p10=float(ang.quantile(.1)),
|
||||
median=float(ang.median()), p90=float(ang.quantile(.9)))
|
||||
out["mu_eq_tan"] = dict(p10=float(mu.quantile(.1)), median=float(mu.median()), p90=float(mu.quantile(.9)))
|
||||
out["note_2026"] = ("2026 事件的 8.85°(=atan(3210/20620))来自‘模拟/粗网格几何’,"
|
||||
"仅作外部量级对照,不能据此断言真实事件的分位或反推 μ 范围。")
|
||||
print(f"角 of reach:n={len(ang)},p10={ang.quantile(.1):.1f}°,中位={ang.median():.1f}°,p90={ang.quantile(.9):.1f}°")
|
||||
print("等效 tan:", {k: round(v, 3) for k, v in out["mu_eq_tan"].items()})
|
||||
with open(os.path.join(RES, "runout_angle_stats.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TPDC 增量的最小审计(DeepSeek 子任务 B)。
|
||||
AI 声明:本脚本由 DeepSeek-V4.1-Flash(DeepSeek)于 2026-10-23 生成,需人工复核。
|
||||
|
||||
只做四件事,全部只读 TPDC_补充数据/下载/*.zip(嵌套包解到 /tmp/opencode 自建目录):
|
||||
1) GFS 灾前预报:源区/口岸逐时次降水率与 6h 累计,核对 lead time、UTC、单位;
|
||||
2) TPCSM:变量、初始化时刻、网格,若可用则取源区降雨;
|
||||
3) 受灾位置点 vs 我们 20.6 km 路径终点;河网 vs 路径;
|
||||
4) 输出 JSON/CSV 到 结果/。
|
||||
不重复下载、不登录、不修改原始 ZIP。
|
||||
"""
|
||||
import os, sys, json, zipfile, shutil, struct
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import netCDF4 as nc
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TPDC = os.path.abspath(os.path.join(ROOT, "..", "TPDC_补充数据", "下载"))
|
||||
RES = os.path.join(ROOT, "结果")
|
||||
TMP = "/tmp/opencode/tpdc_increment"
|
||||
os.makedirs(RES, exist_ok=True)
|
||||
os.makedirs(TMP, exist_ok=True)
|
||||
|
||||
def unnest(zid, inner_ext):
|
||||
"""解外层 zip,再解出内层 zip 中含指定后缀的目录;返回目录路径。"""
|
||||
dst = os.path.join(TMP, zid)
|
||||
if os.path.isdir(dst) and any(f.endswith(inner_ext) for _, _, fs in os.walk(dst) for f in fs):
|
||||
return dst
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
with zipfile.ZipFile(os.path.join(TPDC, zid + ".zip")) as z:
|
||||
z.extractall(dst)
|
||||
for r, _d, fs in os.walk(dst):
|
||||
for f in fs:
|
||||
if f.endswith(".zip"):
|
||||
with zipfile.ZipFile(os.path.join(r, f)) as z:
|
||||
z.extractall(os.path.join(r, "inner"))
|
||||
return dst
|
||||
|
||||
def find(dst, ext):
|
||||
for r, _d, fs in os.walk(dst):
|
||||
for f in fs:
|
||||
if f.endswith(ext):
|
||||
yield os.path.join(r, f)
|
||||
|
||||
# ---------------------------------------------------------------- 1) GFS
|
||||
print("=" * 70); print("1) GFS 灾前预报(起报 2026-08-25 00Z)")
|
||||
gdir = unnest("147456c9-f740-4259-a2ea-fd7423613510", ".nc")
|
||||
gfile = sorted(find(gdir, ".nc"))[0]
|
||||
ds = nc.Dataset(gfile)
|
||||
lat = np.asarray(ds["latitude"][:]); lon = np.asarray(ds["longitude"][:])
|
||||
fh = np.asarray(ds["forecast_hour"][:])
|
||||
init = ds.getncattr("initialization_time")
|
||||
print(f"nc={os.path.basename(gfile)}\n起报={init} 时次={list(map(int,fh))}\n"
|
||||
f"网格 lat{lat.min():.3f}-{lat.max():.3f} lon{lon.min():.3f}-{lon.max():.3f}")
|
||||
|
||||
def idx(la, lo):
|
||||
return int(np.argmin(np.abs(lat - la))), int(np.argmin(np.abs(lon - lo)))
|
||||
|
||||
SRC = (28.28, 85.52) # 源区(公开 AOI 中心)
|
||||
PORT = (28.2785, 85.3766) # 受灾位置元数据
|
||||
rows = []
|
||||
for tag, (la, lo) in [("source", SRC), ("port", PORT)]:
|
||||
i, j = idx(la, lo)
|
||||
rate = np.asarray(ds["precipitation_rate"][:, i, j], float)
|
||||
acc6 = np.asarray(ds["total_precipitation_6h"][:, i, j], float)
|
||||
acci = np.asarray(ds["total_precipitation_since_initialization"][:, i, j], float)
|
||||
cw = np.asarray(ds["cloud_water_path"][:, i, j], float)
|
||||
pw = np.asarray(ds["precipitable_water"][:, i, j], float)
|
||||
for k in range(len(fh)):
|
||||
rows.append(dict(site=tag, grid_lat=float(lat[i]), grid_lon=float(lon[j]),
|
||||
fh=int(fh[k]),
|
||||
valid_utc=str(np.datetime64("2026-08-25T00:00") + np.timedelta64(int(fh[k]), "h")),
|
||||
precip_rate_mm_h=float(rate[k]), precip_6h_mm=float(acc6[k]),
|
||||
precip_cum_mm=float(acci[k]), cloud_water_path=float(cw[k]),
|
||||
precipitable_water=float(pw[k])))
|
||||
g = pd.DataFrame(rows)
|
||||
g.to_csv(os.path.join(RES, "gfs_precip_source_port.csv"), index=False, encoding="utf-8-sig")
|
||||
print(g.to_string(index=False, float_format=lambda v: f"{v:.2f}"))
|
||||
|
||||
# 事件窗:2026-08-26 10:30 BJT = 02:30 UTC,落在 f026-f032 之间 → 用 f030 最近
|
||||
ev = g[(g.valid_utc >= "2026-08-26T00:00") & (g.valid_utc <= "2026-08-26T12:00")]
|
||||
print("\n事件当日 00-12 UTC 预报(源区/口岸):")
|
||||
print(ev[["site", "fh", "valid_utc", "precip_rate_mm_h", "precip_6h_mm"]].to_string(index=False))
|
||||
for tag in ["source", "port"]:
|
||||
sub = g[g.site == tag]
|
||||
# 事件日 6h 累计之和(只加 6h 量,不再与 since-init 相加)
|
||||
ev6 = sub[(sub.valid_utc >= "2026-08-26T00:00") & (sub.valid_utc <= "2026-08-26T18:00")]["precip_6h_mm"].sum()
|
||||
print(f"{tag}: 2026-08-26 00-18UTC 的 6h 累计之和 = {ev6:.1f} mm(仅 6h 量求和)")
|
||||
ds.close()
|
||||
|
||||
# ---------------------------------------------------------------- 2) TPCSM
|
||||
print("\n" + "=" * 70); print("2) TPCSM 5 km 模拟")
|
||||
tdir = unnest("be1c4b80-f26e-4f43-a54a-cedbfe06203b", ".nc")
|
||||
tfiles = sorted(find(tdir, ".nc")) + sorted(find(tdir, ".nc4"))
|
||||
print("nc 文件数:", len(tfiles))
|
||||
if tfiles:
|
||||
t = nc.Dataset(tfiles[0])
|
||||
print("file:", os.path.basename(tfiles[0]))
|
||||
print("dims:", {k: len(v) for k, v in t.dimensions.items()})
|
||||
for n, v in t.variables.items():
|
||||
print(f" {n:<22}{v.dimensions} {v.shape} {v.dtype} {getattr(v,'units','')}")
|
||||
print("attrs:", {k: t.getncattr(k) for k in t.ncattrs()})
|
||||
t.close()
|
||||
else:
|
||||
docx = list(find(tdir, ".docx"))
|
||||
print("未找到 nc;docx:", docx)
|
||||
|
||||
# ---------------------------------------------------------------- 3) 点位与河网
|
||||
print("\n" + "=" * 70); print("3) 受灾位置点 / 河网 vs 我们的路径")
|
||||
def shp_points(path):
|
||||
s = open(path, "rb").read()
|
||||
st = struct.unpack("<i", s[32:36])[0]
|
||||
pos, pts = 100, []
|
||||
while pos < len(s) - 8:
|
||||
_num, clen = struct.unpack(">II", s[pos:pos+8]); clen *= 2
|
||||
body = s[pos+8:pos+8+clen]
|
||||
typ = struct.unpack("<i", body[:4])[0]
|
||||
if typ == 1:
|
||||
x, y = struct.unpack("<2d", body[4:20]); pts.append((x, y))
|
||||
pos += 8 + clen
|
||||
return st, pts
|
||||
|
||||
def shp_lines(path):
|
||||
s = open(path, "rb").read()
|
||||
pos, lines = 100, []
|
||||
while pos < len(s) - 8:
|
||||
_n, clen = struct.unpack(">II", s[pos:pos+8]); clen *= 2
|
||||
body = s[pos+8:pos+8+clen]
|
||||
typ = struct.unpack("<i", body[:4])[0]
|
||||
if typ in (3, 5):
|
||||
nparts, npts = struct.unpack("<2i", body[36:44])
|
||||
parts = struct.unpack("<%di" % nparts, body[44:44+4*nparts])
|
||||
p0 = 44 + 4*nparts
|
||||
xy = np.frombuffer(body[p0:p0+16*npts], dtype="<f8").reshape(-1, 2)
|
||||
for k in range(nparts):
|
||||
a = parts[k]; b = parts[k+1] if k+1 < nparts else npts
|
||||
lines.append(xy[a:b])
|
||||
pos += 8 + clen
|
||||
return lines
|
||||
|
||||
pdir = unnest("72aff770-2f75-459d-a5cc-77812f5b32be", ".shp")
|
||||
st, pts = shp_points(sorted(find(pdir, ".shp"))[0])
|
||||
print("受灾点位 shp 类型", st, "点数", len(pts), "坐标", pts[:3])
|
||||
|
||||
rdir = unnest("e7f8d50b-f647-4efe-8d15-a87173c13ab8", ".shp")
|
||||
lines = shp_lines(sorted(find(rdir, ".shp"))[0])
|
||||
allp = np.vstack(lines)
|
||||
print(f"河网线 {len(lines)} 条,节点 {len(allp)},bbox {allp[:,0].min():.4f}-{allp[:,0].max():.4f}E "
|
||||
f"{allp[:,1].min():.4f}-{allp[:,1].max():.4f}N")
|
||||
|
||||
import math
|
||||
def hav(a, b):
|
||||
R = 6371.0
|
||||
p1, p2 = math.radians(a[1]), math.radians(b[1])
|
||||
dp = p2-p1; dl = math.radians(b[0]-a[0])
|
||||
return 2*R*math.asin(math.sqrt(math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2))
|
||||
|
||||
path = pd.read_csv(os.path.join(RES, "q4_path_profile.csv"))
|
||||
end = (float(path.lon.iloc[-1]), float(path.lat.iloc[-1]))
|
||||
dmg = pts[0]
|
||||
print(f"路径终点 ({end[0]:.4f},{end[1]:.4f}) 高程 {path.elev_m.iloc[-1]:.0f} m;"
|
||||
f"受灾点 ({dmg[0]:.4f},{dmg[1]:.4f});距离 {hav(end, dmg):.2f} km")
|
||||
# 路径每点 1 km 内是否有河网节点(粗判路径是否沿河道)
|
||||
res = []
|
||||
for lo, la in zip(path.lon, path.lat):
|
||||
dmin = min(hav((lo, la), (x, y)) for x, y in allp)
|
||||
res.append(dmin)
|
||||
path["dist_to_river_km"] = res
|
||||
path.to_csv(os.path.join(RES, "q4_path_profile_with_river.csv"), index=False, encoding="utf-8-sig")
|
||||
print(f"路径到最近河网节点:中位 {np.median(res):.3f} km,p90 {np.percentile(res,90):.3f} km,最大 {max(res):.3f} km")
|
||||
print("前 6 个路径点的最近河距 (km):", [round(x,3) for x in res[:6]])
|
||||
print("后 6 个路径点的最近河距 (km):", [round(x,3) for x in res[-6:]])
|
||||
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MathorCup 2026 赛道B 初步试解(DeepSeek 子任务 B)
|
||||
AI 辅助说明:本脚本由 DeepSeek-V4.1-Flash(DeepSeek)于 2026-10-23 生成,
|
||||
用于竞赛初步评估;核心建模与结论需参赛队人工复核后方可采用。
|
||||
|
||||
做三件事(全部只读原始附件,结果写到本目录 结果/):
|
||||
Q1 基线:候选气象/土壤/辐射特征在事件窗相对同季同时刻对照的百分位排序,并做按年 bootstrap 稳定性。
|
||||
Q4 基线:在真实 DEM 上从公开核定的源区出发做 D8 汇流路径 + Voellmy 型一维运动积分,
|
||||
输出路径长度/落差/到达时间/速度,并对摩擦参数与流深做敏感性。
|
||||
数据核查:附件冰川 NetCDF 的 UTM 坐标与本地 DEM、以及公开 AOI 坐标是否一致。
|
||||
|
||||
不做的:不把情景区间冒充 95% 置信区间;不把无记录日当负样本;不宣称预报能力。
|
||||
"""
|
||||
import os, re, json, struct
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from PIL import Image
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # B_DeepSeek试解
|
||||
ATT = os.path.abspath(os.path.join(ROOT, "..", "..", "附件")) # MathorCup2026/附件
|
||||
EXT = os.path.join(ROOT, "外部数据")
|
||||
RES = os.path.join(ROOT, "结果")
|
||||
os.makedirs(RES, exist_ok=True)
|
||||
|
||||
TMP = "/tmp/opencode/b_solve" # 临时解压目录
|
||||
os.makedirs(TMP, exist_ok=True)
|
||||
|
||||
# ---------------------------------------------------------------- 通用读取
|
||||
def parse_ts(s):
|
||||
s = str(s).strip().replace("/", "-")
|
||||
m = re.match(r"(\d{4})-(\d{1,2})-(\d{1,2})[ ](\d{1,2}):(\d{2})", s)
|
||||
if not m:
|
||||
return None
|
||||
y, mo, d, h, mi = m.groups()
|
||||
return f"{int(y):04d}-{int(mo):02d}-{int(d):02d}T{int(h):02d}:{mi}:00"
|
||||
|
||||
def load_table(kind, year):
|
||||
"""读取附件(1)解压后的某类表;9999.9 置为缺失。"""
|
||||
f = os.path.join(TMP, "met", f"{kind}_Gyirong_{year}.csv")
|
||||
df = pd.read_csv(f, encoding="utf-8-sig")
|
||||
df.columns = [c.strip().replace("\ufeff", "") for c in df.columns]
|
||||
t = np.array([parse_ts(x) for x in df["Timestamp"]], dtype="datetime64[m]")
|
||||
num = df.drop(columns=["Timestamp"]).apply(pd.to_numeric, errors="coerce")
|
||||
a = num.to_numpy(dtype=float)
|
||||
a[np.isclose(a, 9999.9)] = np.nan
|
||||
return t, pd.DataFrame(a, columns=num.columns)
|
||||
|
||||
def ensure_met():
|
||||
if os.path.isdir(os.path.join(TMP, "met")):
|
||||
return
|
||||
import zipfile
|
||||
z = [f for f in os.listdir(ATT) if f.startswith("(1)") and f.endswith(".zip")][0]
|
||||
with zipfile.ZipFile(os.path.join(ATT, z)) as zf:
|
||||
zf.extractall(os.path.join(TMP, "met"))
|
||||
|
||||
# ---------------------------------------------------------------- Q1 基线
|
||||
print("=" * 70)
|
||||
print("Q1 候选特征百分位基线")
|
||||
print("=" * 70)
|
||||
ensure_met()
|
||||
EV = np.datetime64("2026-08-26T10:00")
|
||||
EV_DOY = 238
|
||||
|
||||
FEATS = [
|
||||
("precip_1h", "mm/h"), ("precip_3h", "mm/3h"), ("precip_24h", "mm/24h"), ("precip_72h", "mm/72h"),
|
||||
("Ta_2m", "degC"), ("dT_2_10", "degC"), ("RH_2m", "%"), ("WS_10m", "m/s"),
|
||||
("shear_1_20", "1/s"), ("dpres_3h", "hPa/3h"), ("VWC_0.1", "m3/m3"),
|
||||
("dVWC_24h", "m3/m3/d"), ("soilT_0.1", "degC"), ("netrad", "W/m2"),
|
||||
]
|
||||
|
||||
def build_features(year):
|
||||
"""把 MET+SOIL+RADM 拼成小时特征表;全部按原始时间轴对齐。"""
|
||||
tm, m = load_table("MET", year)
|
||||
ts, s = load_table("SOIL", year)
|
||||
tr, r = load_table("RADM", year)
|
||||
assert (tm == ts).all() and (tm == tr).all()
|
||||
p = m["Precip(mm)"].to_numpy()
|
||||
out = {}
|
||||
out["precip_1h"] = p
|
||||
for k, w in [(3, 3), (24, 24), (72, 72)]:
|
||||
c = pd.Series(p).rolling(w, min_periods=w).sum().to_numpy()
|
||||
out[f"precip_{k}h"] = c
|
||||
out["Ta_2m"] = m["Ta_2m(degree C)"].to_numpy()
|
||||
out["dT_2_10"] = m["Ta_2m(degree C)"].to_numpy() - m["Ta_10m(degree C)"].to_numpy()
|
||||
out["RH_2m"] = m["RH_2m(%)"].to_numpy()
|
||||
out["WS_10m"] = m["WS_10m(m/s)"].to_numpy()
|
||||
out["shear_1_20"] = (m["WS_20m(m/s)"].to_numpy() - m["WS_1m(m/s)"].to_numpy()) / 19.0
|
||||
pres = pd.Series(m["Pressure(hPa)"].to_numpy())
|
||||
out["dpres_3h"] = pres.diff(3).to_numpy()
|
||||
vwc = s["VWC_0.1m(m3/m3)"].to_numpy()
|
||||
out["VWC_0.1"] = vwc
|
||||
out["dVWC_24h"] = pd.Series(vwc).diff(24).to_numpy()
|
||||
out["soilT_0.1"] = s["soilT_0.1m(degree C)"].to_numpy()
|
||||
out["netrad"] = (r["Rsd(W/m2)"].to_numpy() - r["Rsu(W/m2)"].to_numpy()
|
||||
+ r["Rld(W/m2)"].to_numpy() - r["Rlu(W/m2)"].to_numpy())
|
||||
return tm, out
|
||||
|
||||
data = {}
|
||||
for y in range(2021, 2027):
|
||||
data[y] = build_features(y)
|
||||
|
||||
def matched_values(year, name, doy=EV_DOY, hour=10, win=20):
|
||||
t, f = data[year]
|
||||
doy_v = (t.astype("datetime64[D]") - t.astype("datetime64[Y]").astype("datetime64[D]")).astype(int) + 1
|
||||
hh = t.astype("datetime64[h]").astype(int) % 24
|
||||
sel = (((np.abs(doy_v - doy) <= win) | (np.abs(doy_v - doy) >= 365 - win)) & (hh == hour))
|
||||
v = f[name][sel]
|
||||
return v[np.isfinite(v)]
|
||||
|
||||
def pct_rank(x, base):
|
||||
base = base[np.isfinite(base)]
|
||||
if not np.isfinite(x) or len(base) < 20:
|
||||
return np.nan
|
||||
return 100.0 * ((base < x).sum() + 0.5 * (base == x).sum()) / len(base)
|
||||
|
||||
rows = []
|
||||
for name, unit in FEATS:
|
||||
te, fe = data[2026]
|
||||
i = int(np.argmin(np.abs(te - EV)))
|
||||
x = fe[name][i]
|
||||
base = np.concatenate([matched_values(y, name) for y in range(2021, 2026)])
|
||||
p = pct_rank(x, base)
|
||||
rows.append(dict(feature=name, unit=unit, event_value=None if not np.isfinite(x) else float(x),
|
||||
pctl_vs_season=p, n_control=int(len(base)),
|
||||
extremeness=abs(p - 50) if np.isfinite(p) else np.nan))
|
||||
q1 = pd.DataFrame(rows).sort_values("extremeness", ascending=False)
|
||||
q1.to_csv(os.path.join(RES, "q1_feature_percentiles.csv"), index=False, encoding="utf-8-sig")
|
||||
print(q1.to_string(index=False, float_format=lambda v: f"{v:.3f}"))
|
||||
|
||||
# 按年 bootstrap:5 个对照年有放回重抽,看排序位次稳定性
|
||||
rng = np.random.default_rng(20261023)
|
||||
boot_rank = {f: [] for f, _ in FEATS}
|
||||
for _ in range(500):
|
||||
yrs = rng.choice([2021, 2022, 2023, 2024, 2025], size=5, replace=True)
|
||||
vals = {}
|
||||
for name, _unit in FEATS:
|
||||
te, fe = data[2026]
|
||||
i = int(np.argmin(np.abs(te - EV)))
|
||||
base = np.concatenate([matched_values(y, name) for y in yrs])
|
||||
p = pct_rank(fe[name][i], base)
|
||||
vals[name] = abs(p - 50) if np.isfinite(p) else -1
|
||||
order = sorted(vals, key=lambda k: -vals[k])
|
||||
for r, k in enumerate(order, 1):
|
||||
boot_rank[k].append(r)
|
||||
stab = pd.DataFrame([
|
||||
dict(feature=k, rank_mean=float(np.mean(v)), rank_p05=float(np.percentile(v, 5)),
|
||||
rank_p95=float(np.percentile(v, 95)), top5_freq=float(np.mean(np.array(v) <= 5)))
|
||||
for k, v in boot_rank.items()]).sort_values("rank_mean")
|
||||
stab.to_csv(os.path.join(RES, "q1_rank_stability.csv"), index=False, encoding="utf-8-sig")
|
||||
print("\n按年 bootstrap 排序稳定性(500 次,5 年有放回):")
|
||||
print(stab.to_string(index=False, float_format=lambda v: f"{v:.2f}"))
|
||||
|
||||
# ---------------------------------------------------------------- 数据核查
|
||||
print("\n" + "=" * 70)
|
||||
print("数据核查:附件冰川 NetCDF 与本底 DEM / 公开 AOI 的坐标一致性")
|
||||
print("=" * 70)
|
||||
import zipfile as _zf
|
||||
if not os.path.isdir(os.path.join(TMP, "dem")):
|
||||
_dz = [f for f in os.listdir(ATT) if f.startswith("(7)") and f.endswith(".zip")][0]
|
||||
with _zf.ZipFile(os.path.join(ATT, _dz)) as z:
|
||||
z.extractall(os.path.join(TMP, "dem"))
|
||||
dem = np.array(Image.open(os.path.join(TMP, "dem", "DEM.tif"))).astype(float)
|
||||
dem[dem <= -32767] = np.nan
|
||||
nx, ny = dem.shape[1], dem.shape[0]
|
||||
px, py = 0.0002777467191455384, 0.00027774196409497727
|
||||
x0, y0 = 85.18975577464204, 28.564382284579324
|
||||
|
||||
import netCDF4 as nc
|
||||
gz = [f for f in os.listdir(ATT) if f.startswith("(4)") and f.endswith(".zip")][0]
|
||||
import zipfile
|
||||
if not os.path.isdir(os.path.join(TMP, "glacier")):
|
||||
with zipfile.ZipFile(os.path.join(ATT, gz)) as zf:
|
||||
zf.extractall(os.path.join(TMP, "glacier"))
|
||||
gfile = os.path.join(TMP, "glacier", [f for f in os.listdir(os.path.join(TMP, "glacier")) if f.endswith(".nc")][0])
|
||||
ds = nc.Dataset(gfile)
|
||||
gx = np.asarray(ds["x"][:]); gy = np.asarray(ds["y"][:]); gdem = np.asarray(ds["dem"][:], float)
|
||||
def utm_inv(E, N, lon0=87.0, k0=0.9996, E0=500000.0):
|
||||
a = 6378137.0; f = 1/298.257223563; e2 = 2*f - f*f; ep2 = e2/(1-e2)
|
||||
x = E - E0; M = N/k0
|
||||
mu = M/(a*(1 - e2/4 - 3*e2**2/64 - 5*e2**3/256))
|
||||
e1 = (1-np.sqrt(1-e2))/(1+np.sqrt(1-e2))
|
||||
p1 = (mu + (3*e1/2 - 27*e1**3/32)*np.sin(2*mu) + (21*e1**2/16 - 55*e1**4/32)*np.sin(4*mu)
|
||||
+ (151*e1**3/96)*np.sin(6*mu) + (1097*e1**4/512)*np.sin(8*mu))
|
||||
C1 = ep2*np.cos(p1)**2; T1 = np.tan(p1)**2
|
||||
N1 = a/np.sqrt(1-e2*np.sin(p1)**2); R1 = a*(1-e2)/(1-e2*np.sin(p1)**2)**1.5
|
||||
D = x/(N1*k0)
|
||||
lat = p1 - (N1*np.tan(p1)/R1)*(D**2/2 - (5+3*T1+10*C1-4*C1**2-9*ep2)*D**4/24
|
||||
+ (61+90*T1+298*C1+45*T1**2-252*ep2-3*C1**2)*D**6/720)
|
||||
lon = np.radians(lon0) + (D - (1+2*T1+C1)*D**3/6
|
||||
+ (5-2*C1+28*T1-3*C1**2+8*ep2+24*T1**2)*D**5/120)/np.cos(p1)
|
||||
return np.degrees(lon), np.degrees(lat)
|
||||
glo, gla = utm_inv(gx.mean(), gy.mean())
|
||||
aoi = json.load(open(os.path.join(EXT, "zenodo_22717809_gyirong_precursors", "detachment_aoi.geojson")))
|
||||
def flat(c):
|
||||
if isinstance(c[0], (int, float)):
|
||||
return [c]
|
||||
out = []
|
||||
for z in c:
|
||||
out += flat(z)
|
||||
return out
|
||||
pts = flat(aoi["features"][0]["geometry"]["coordinates"])
|
||||
alon = [p[0] for p in pts]; alat = [p[1] for p in pts]
|
||||
print(f"nc 质心 lon={glo:.5f} lat={gla:.5f};公开 AOI bbox lon[{min(alon):.4f},{max(alon):.4f}] "
|
||||
f"lat[{min(alat):.4f},{max(alat):.4f}];质心是否落在 AOI 内:"
|
||||
f"{min(alon)<=glo<=max(alon) and min(alat)<=gla<=max(alat)}")
|
||||
# nc dem 与本地 DEM 重叠格点比对
|
||||
lons = gx; lats = utm_inv(gx[:, None]*np.ones((1, len(gy))), (gy[None, :]*np.ones((len(gx), 1))).T)[1] \
|
||||
if False else None
|
||||
# 逐格点反算(145x138=20010,可承受)
|
||||
xx, yy = np.meshgrid(gx, gy) # shape (138,145)
|
||||
llon, llat = utm_inv(xx, yy)
|
||||
col = ((llon - x0)/px).astype(int); row = ((y0 - llat)/py).astype(int)
|
||||
ok = (col >= 0) & (col < nx) & (row >= 0) & (row < ny) & np.isfinite(gdem)
|
||||
d_local = dem[row[ok], col[ok]]
|
||||
d_nc = gdem[ok]
|
||||
diff = d_nc - d_local
|
||||
print(f"重叠格点 {ok.sum()};本地 DEM 均值 {np.nanmean(d_local):.0f} m,nc 均值 {np.nanmean(d_nc):.0f} m;"
|
||||
f"差值中位 {np.nanmedian(diff):+.1f} m,MAE {np.nanmean(np.abs(diff)):.1f} m")
|
||||
ds.close()
|
||||
|
||||
# ---------------------------------------------------------------- Q4 传播基线
|
||||
print("\n" + "=" * 70)
|
||||
print("Q4 情景传播基线:真实 DEM 上 D8 路径 + Voellmy 型一维运动积分")
|
||||
print("=" * 70)
|
||||
from scipy.ndimage import minimum_filter
|
||||
# 选路网格:4x4 块平均降到约 120 m 再 3x3 侵蚀填洼。
|
||||
# 30 m 单流向会卡在局部洼地(实测 10.6 km 处终止),120 m 网格可贯通到口岸河段。
|
||||
K = 4
|
||||
ny2, nx2 = ny//K, nx//K
|
||||
dem_route = dem[:ny2*K, :nx2*K].reshape(ny2, K, nx2, K).mean(axis=(1, 3))
|
||||
dem_route = minimum_filter(dem_route, size=3)
|
||||
dzdy, dzdx = np.gradient(dem, 30.7, 27.2)
|
||||
slope = np.degrees(np.arctan(np.hypot(dzdx, dzdy)))
|
||||
|
||||
src_lon, src_lat = float(np.mean(alon)), float(np.mean(alat))
|
||||
sc = int((src_lon - x0)/(px*K)); sr = int((y0 - src_lat)/(py*K))
|
||||
print(f"源区中心 (lon={src_lon:.4f}, lat={src_lat:.4f}) -> 行 {sr}, 列 {sc}, "
|
||||
f"DEM 高程 {dem[sr, sc]:.0f} m, 坡度 {slope[sr, sc]:.1f}°")
|
||||
|
||||
def lowest_neighbor(r, c, rad=3):
|
||||
"""D8 下沉;遇洼地在 rad 半径内找最低格点(简化 breach)。"""
|
||||
best = None
|
||||
for dr in range(-rad, rad+1):
|
||||
for dc in range(-rad, rad+1):
|
||||
if dr == 0 and dc == 0:
|
||||
continue
|
||||
rr, cc = r+dr, c+dc
|
||||
if not (0 <= rr < ny2 and 0 <= cc < nx2):
|
||||
continue
|
||||
if not np.isfinite(dem[rr, cc]):
|
||||
continue
|
||||
if dem_route[rr, cc] < dem_route[r, c] - 0.01:
|
||||
d = np.hypot(dr*30.7*K, dc*27.2*K)
|
||||
key = (dem_route[rr, cc], d)
|
||||
if best is None or key < best[0]:
|
||||
best = (key, rr, cc)
|
||||
return (best[1], best[2]) if best else (None, None)
|
||||
|
||||
path = [(sr, sc)]
|
||||
seen = {(sr, sc)}
|
||||
while len(path) < 20000:
|
||||
r, c = path[-1]
|
||||
if dem_route[r, c] < 1850: # 到达口岸河段高程量级
|
||||
break
|
||||
rr, cc = lowest_neighbor(r, c, rad=3)
|
||||
if rr is None or (rr, cc) in seen:
|
||||
# 允许一次半径外扩,仍失败则停
|
||||
rr, cc = lowest_neighbor(r, c, rad=6)
|
||||
if rr is None or (rr, cc) in seen:
|
||||
break
|
||||
seen.add((rr, cc))
|
||||
path.append((rr, cc))
|
||||
path = np.array(path)
|
||||
# 高程用真实 30 m DEM 采样(粗网格行/列映射回 30 m),沿路径 5 点中值平滑抑制 DEM 噪声
|
||||
zc = dem[path[:, 0]*K + K//2, path[:, 1]*K + K//2]
|
||||
prof_z = pd.Series(zc).rolling(5, center=True, min_periods=1).median().to_numpy()
|
||||
step = np.hypot(np.diff(path[:, 0])*30.7*K, np.diff(path[:, 1])*27.2*K)
|
||||
dist = np.concatenate([[0], np.cumsum(step)])/1000.0
|
||||
print(f"路径格点 {len(path)},水平长度 {dist[-1]:.2f} km,落差 {prof_z[0]-prof_z[-1]:.0f} m,"
|
||||
f"末端高程 {prof_z[-1]:.0f} m")
|
||||
pd.DataFrame(dict(km=dist, elev_m=prof_z,
|
||||
lon=x0+path[:, 1]*px*K, lat=y0-path[:, 0]*py*K)).to_csv(
|
||||
os.path.join(RES, "q4_path_profile.csv"), index=False, encoding="utf-8-sig")
|
||||
|
||||
G = 9.81
|
||||
def voellmy(mu, xi, h, v0=0.0, z=None, s=None, dt=0.02):
|
||||
"""沿路径推进的 Voellmy 型一维运动(固定时间步 + 节点插值)。
|
||||
dv/dt = g sinθ - g μ cosθ - g v²/(ξ h),θ 取当前节点到下一点的坡角。
|
||||
返回每个路径节点的到达时间 t 与速度 v。"""
|
||||
z = prof_z if z is None else z
|
||||
s = step if s is None else s
|
||||
L = float(np.sum(s))
|
||||
SN = np.concatenate([[0.0], np.cumsum(s)]) # 节点累计里程
|
||||
vt = np.zeros(len(SN)); vt[0] = v0
|
||||
ar = np.zeros(len(SN))
|
||||
v = max(v0, 0.1); t = 0.0; pos = 0.0; i = 0
|
||||
while pos < L and t < 20000:
|
||||
i = min(np.searchsorted(SN, pos, side="right") - 1, len(s)-1)
|
||||
th = np.arctan2(max(z[i]-z[i+1], 1e-6), s[i])
|
||||
a = G*np.sin(th) - G*mu*np.cos(th) - G*v*v/(abs(xi)*h)
|
||||
v = max(v + a*dt, 0.5) # 0.5 m/s 下限代表惯性,避免数值上“冻结”
|
||||
pos += v*dt
|
||||
t += dt
|
||||
while i+1 < len(SN) and SN[i+1] <= pos:
|
||||
vt[i+1] = v; ar[i+1] = t; i += 1
|
||||
arr = ar.copy()
|
||||
arr[arr == 0] = t
|
||||
return vt, arr
|
||||
|
||||
scen = []
|
||||
for mu in [0.05, 0.10, 0.20]:
|
||||
for xi in [500, 1000, 2000]:
|
||||
for h in [10.0, 25.0]:
|
||||
v, tt = voellmy(mu, xi, h)
|
||||
scen.append(dict(mu=mu, xi_m_s2=xi, h_m=h, v_max_m_s=float(np.nanmax(v)),
|
||||
t_total_s=float(tt[-1]), t_total_min=float(tt[-1]/60),
|
||||
v_end_m_s=float(v[-1]), runout_km=float(dist[-1])))
|
||||
sc = pd.DataFrame(scen).sort_values("t_total_s")
|
||||
sc.to_csv(os.path.join(RES, "q4_voellmy_sensitivity.csv"), index=False, encoding="utf-8-sig")
|
||||
print(sc.to_string(index=False, float_format=lambda v: f"{v:.1f}"))
|
||||
|
||||
# 中位参数的代表情景,逐段到达时间
|
||||
v_ref, t_ref = voellmy(0.10, 1000, 25.0)
|
||||
marks = []
|
||||
for k in np.arange(0, dist[-1]+0.001, 2.0):
|
||||
if k <= 0:
|
||||
continue
|
||||
j = int(np.argmin(np.abs(dist - k)))
|
||||
marks.append(dict(km=float(dist[j]), t_min=float(t_ref[j]/60), v_m_s=float(v_ref[j]), elev=float(prof_z[j])))
|
||||
pd.DataFrame(marks).to_csv(os.path.join(RES, "q4_arrival_marks.csv"), index=False, encoding="utf-8-sig")
|
||||
print("\n代表情景 (μ=0.10, ξ=1000, h=25 m) 每 2 km 到达时间:")
|
||||
print(pd.DataFrame(marks).to_string(index=False, float_format=lambda v: f"{v:.1f}"))
|
||||
Reference in new issue
Block a user