← Back to skills
extension
Category: Data & AnalyticsNo API key required

Trail running race plan

把任意越野/路跑赛事的 GPX + 检查点/关门信息,流水线式生成一份自包含、可实时调参的交互式「分站配速目标表」(race plan / 完赛预测表)。输入 = 赛事 GPX 文件 + 起点/各检查点官方里程 + (可选)分段 D+/D− 与关门时刻 + (可选)天气数据;输出 = 单个 HTML + 可导出 CSV。含天气因子(v2):气温 / 落雨 / 大风三项,按各站到达时刻显示温度与降水、风力徽章,并按发枪起算的小时时间轴展示(默认近三年同期气候基准,赛事 ≤7 天则自动改用天气预报);等效距离计入下降,且面板「路况」可切干地 / 小雨 / 泥泞,泥泞时自动加大技术下坡成本。触发词:race plan、配速表、完赛预测表、赛事预测表、分站时刻表、目标完赛时间、GPX 配速、aid station splits、检查点到达时刻、关门时间配速、把 GPX 做成配速计划、天气因子、落雨预测、大风预测、气温区间、赛道温度、天气预报配速、泥泞路面。

personAuthor: user_c6c808b6hubcommunity

race-plan · GPX → 交互式分站配速目标表

把「赛事 GPX + 官方检查点/关门数据」变成一份可直接给选手用的分站配速目标表。

一句话流程:写 race_config.json → build_course.py 出 course.json → build_html.py 出 HTML。

与 ultra-gpx-pace-plan 的分工:本 skill 是快速标准产线(只要 GPX + 检查点数据就能出可交互配速表);若用户需要「用选手历史完赛数据校准目标 / 判断某目标是否现实 / 叠加气象温度曲线 / 对自述症状做分段复盘」,在本 skill 产出 course.json 后转交 ultra-gpx-pace-plan 的进阶脚本。

何时使用

  • 用户给出赛事 GPX(含/不含 waypoint),要「X–Y 小时完赛」的分站到达/出发时刻表、配速计划、关门核对。
  • 要一份能现场调参的表(改爬升折算、改后程降速、改各站停留,时刻立刻重算)+ 高程剖面 + CSV 导出。
  • 比赛日有雨/大风风险,需要在各站到达时刻看到降水强度、概率、风速阵风,并据此调整「路况」(干地 / 泥泞)。
  • 复现/更新某场已做过的 race plan(内置三个可跑样例:examples/yunqiushan50k/(云丘山 MMY 50K,GPX 1.0)、examples/chaigu50k/(柴古括苍 50K,GPX 1.1 + 关门推算)、examples/moganshan100k/(莫干山 100K,跨两夜枪时轴天气 + 雨战默认路况))。

工作流

1. 收集输入

| 必需 | 说明 | 来源 | |---|---|---| | 赛事 GPX | 轨迹(<trkpt>),可含检查点 <wpt> | 用户文件 / 赛事官网 | | 检查点官方里程 | 起点 + 每个 CP 的累计 km | 赛事手册 / 公众号图 / 官网 | | 分段 D+/D− | 每段爬升/下降(可选,但强烈建议) | 同上 | | 关门时刻 | 各站关门钟点(可选,几个关键站即可) | 竞赛规程 | | 气温数据 | (可选)近三年同期气温或比赛日天气预报 | 气象网站 / 历史气象记录 |

补充:

  • 官方分段值优先:若手册/赛事图给了每段 D+/D− 且求和 == 官方总 D+(校验锁定),直接用官方值填 seg_gain/seg_loss;GPX 推算只作交叉核对列。点对点起伏大的段,GPX 毛值可能是官方平滑值的两倍,属正常,别据此怀疑数据。
  • 只公布少数关门点很常见(如云丘山只有 M2/M4/终点)——没公布的就 null,靠 banner 点出「最紧的闸门」。
  • 读赛事图取数:用 PIL 裁到表格带再放大 2–5× 复核;低清整图容易把 1 读成 2。
  • GPX waypoint 名若形如 M1-后庄,默认按 sym 子串匹配即可;若 waypoint 名不含编号,在 station 里给 match 字段,或直接给 lat/lon 按坐标吸附。
  • 环形赛道(终点=起点,只有「起终点」一个 waypoint):设 "loop": true,脚本会把 F 锚到轨迹最后一点。

2. 写 race_config.json

复制 scripts/race_config.template.json 改。完整可用样例见 examples/yunqiushan50k/race_config.json。

顶层字段: | 字段 | 含义 | |---|---| | race / date / startTime / venue | 标题、比赛日、发枪钟点、地点(venue 可选) | | total_km / total_gain / total_loss | 官方全程数据(填在顶栏 stats) | | cutoff_h | 总关门小时 | | climb_km | 爬升折算:默认 100(100 m 爬升 ≈ 1 km 平地)。越小=爬升越重 | | loss_km | 下降折算:默认 250(250 m 下降 ≈ 1 km 平地,即下降按爬升约 2.5 倍折算)。面板「路况=泥泞」会自动缩小该值 | | beta | 后程降速幅度:默认 0.38。首段→末段等效配速漂移(50K 常取 0.35–0.42) | | official | 可选:官方预测基线,结构 {"name":"…","arrivals":{"M2":240, "F":780}}(值为相对起跑分钟);会在天气时间轴标绿「官」刻度,便于对照 | | loop | 是否环形赛道(终点=起点) | | targets | 目标完赛小时数组,如 [18,20,22,24,26,28,30];省略则按 cutoff_h 自动给 4 档 | | rec_idx | 哪一档标「推荐基准」(0 起);默认倒数第二 | | cond_default | 可选:"dry"(默认)/ "mud"。预报有雨/赛道泥泞时建议设 "mud",避免开表就是偏乐观的干地节奏 | | csv_name | 导出 CSV 文件名 | | title/subtitle/banner/notes/chart_desc | 可选文案覆盖(banner 为 HTML;notes 为 <li>…</li> 数组);省略则按数据自动生成 |

stations[](按里程顺序排列): | 字段 | 含义 | |---|---| | sym | 站点代号(S/M1…/F),用于匹配 GPX waypoint 与表内标记 | | name / en | 中文名 / 英文名(英文可选) | | kind | start / aid / water / finish(决定徽章配色) | | off_km | 官方累计里程(对齐基准) | | seg_gain / seg_loss | 该段爬升/下降(官方优先) | | cutoff_h / cutoff_clock | 该站关门(相对起跑小时数 / 钟点),无则 null | | cutoff_estimated | 可选:true 表示该站关门为推算值(官方未公布),表中标注「推算」;官方公布的关门不填 | | stop | 计划停留(分钟) | | match / lat / lon | 可选:GPX waypoint 匹配子串 / 坐标(无 waypoint 时按坐标吸附) | | seg_factor | 可选:该段(进入本站的赛段)强度系数,>1 表示该段更吃力(如长技术下坡、泥泞段);默认 1.0 | | exposed | 可选:true 表示本站位于暴露山脊/风口,风力徽章加 ⚠ 提示 |

2b. 天气因子 weather(可选,推荐)

加入 weather 顶层块即开启。三种指标(气温 / 降水 / 风力)共用同一来源判定(auto:比赛日距今 ≤7 天且给了预报 → 用天气预报;否则近三年同期平均)。表格每个到达时刻单元格显示温度/降水/风力徽章;新增「天气时间轴」按比赛日钟点展示降水柱状 + 气温/风力曲线 + 各站到达竖线;卡片显示推荐目标的气温区间、预计降水总量、最大风力;CSV 增列各档气温/降水/风力。

最简做法:用 scripts/fetch_weather.py(Open-Meteo 免 key)按赛道 lat/lon + 比赛日一键拉取,直接生成可粘贴的 weather 块(见 §3b)。示例目录里的 *_history.csv 与 forecast_hours 为演示样例,真实赛事请替换为真实数据。

| 字段 | 含义 | |---|---| | mode | "auto"(默认)/ "climatology" / "forecast" | | race_date | 比赛日 YYYY-MM-DD(用于算「距今几天」并匹配历史 MM-DD) | | ref_ele | 气温数据对应的参考海拔(m);各站按 (ele−ref_ele)/100×0.6 做海拔递减修正 | | 气温 | history_csv(列 date,tmin,tmax) / climatology_hours(24 数组) / forecast_hours(枪时轴) / forecast_csv(列 hour,temp) | | 降水 | precip_history_csv(列 date,psum[,pprob]) / precip_climatology_hours(24, mm) / precip_forecast_hours(枪时轴, mm) / precip_forecast_prob_hours(枪时轴, %) / precip_forecast_csv(列 hour,precip[,prob]) | | 风力 | wind_history_csv(列 date,wspd[,wgust]) / wind_climatology_hours(24, m/s) / wind_forecast_hours(枪时轴, m/s) / wind_forecast_gust_hours(枪时轴, m/s) / wind_forecast_csv(列 hour,wind[,gust]) | | <prefix>hours_axis | 可选:"clock"(钟点轴,数组长 24)/ "gun"(枪时轴,数组长 = 比赛窗口小时数)。缺省自动推断(见下) | | note | 可选:覆盖自动生成的天气来源说明 |

⚠ 时间轴口径(最易出错的地方)

天气按发枪后小时数(timeline[0] = 起跑那一刻)索引,不是按钟点索引。因为一场 30 h 的百公里会跨越两个夜晚:按钟点索引会把第一天的雨当成第二天的雨——对气温的日周期无伤大雅,对降水则是完全错误。

  • *_climatology_hours / CSV / history_csv:钟点轴,长度 24(日周期),由 weatherlib.to_timeline() 按起跑钟点平铺成枪时轴。
  • *_forecast_hours:枪时轴,长度 = 比赛窗口小时数(fetch_weather.py 按 --start/--hours 切片得到)。
  • 自动推断:数组长度 == n_hours 且 n_hours != 24 → 判为枪时轴,否则判为钟点轴。拿不准就显式写 hours_axis。
  • n_hours 由 build_course.py 自动定:ceil(max(targets)) + 2。
  • 每个指标块同时输出 timeline(枪时轴,主用)与 profile(钟点轴,24,向下兼容旧 course.json)。旧的扁平 weather.profile 仍能渲染(前端 expand() 会自动平铺)。

展示与模型联动:气温/降水/风力均为展示 + 提示,不直接进 pace 模型(与气温一致)。但面板保留「路况」开关(干地 / 泥泞),泥泞时自动加大下降等效成本(loss_km 按 1.0 / 0.45 缩放),落地云丘山 2026 雨战复盘结论(泥泞技术下坡被严重低估)。降水 climatology 波动大,优先走预报。

数据口径提示:*_history_csv 是真实可复现的三年均值来源;各类 forecast_* 请在赛事进入 7 天窗口后用真实预报(fetch_weather.py)替换。降水 climatology 没有自然日内形态,按日均平铺到 24h 并保留 daily_mm。若所有数据都缺,气温回退中性 12 °C 平面曲线,降水/风力回退为空(不显示对应徽章)。

3. 跑脚本

3b.(可选,推荐)一键拉天气:若要做降水/风力,先按赛道 lat/lon 拉取,免手写数组:

PY=/Users/songyao/.workbuddy/binaries/python/envs/default/bin/python
$PY scripts/fetch_weather.py --lat 30.60 --lon 119.90 --date 2026-10-23 --ref_ele 100 \
     --start 18:00 --hours 32 --out weather_block.json --print
# --start  发枪钟点(默认 08:00),决定预报从哪一小时切起
# --hours  比赛窗口小时数(默认 48);建议 ceil(max(targets))+2
# --merge <race_config.json>  直接把 weather 块并进配置

--hours 必须覆盖整场比赛窗口,否则后段到达时刻会取到被截断的最后一项。 拉到的块含 forecast_hours / precip_forecast_hours / precip_forecast_prob_hours / wind_forecast_hours / wind_forecast_gust_hours,mode:"auto"。进入 7 天窗口自动走预报;否则走近三年同期 climatology。该脚本联网,跑一次即可把结果固化进 config(无需每次 build 重拉)。

PY=/Users/songyao/.workbuddy/binaries/python/envs/default/bin/python
$PY scripts/build_course.py <race_config.json> <course.gpx> <out/course.json>
$PY scripts/build_html.py   <out/course.json>   <out/race_plan.html>

build_course.py 会:吸附各站到轨迹 → 用官方里程锚定(GPX 实测与官方按段比例对齐)→ 段内重采样出海拔剖面 → 输出含模型参数与自动文案的 course.json。若配置含 weather,还会调用 scripts/weatherlib.py 按 n_hours = ceil(max(targets)) + 2 生成枪时轴气温/降水/风力曲线(每个指标同时给出 24 长度钟点轴 profile 以兼容旧文件)并写入 course.json(也可单独跑 weatherlib.py <config> <out.json> <n_hours> 预览)。 控制台会打印每站的 snap(站到轨迹的可疑偏差,正常应 < 30 m)与 D+ 交叉核对。

4. 验证(交付前必做)

4a. 一键自检(改完脚本先跑这个,纯离线、约几秒):

PY=/Users/songyao/.workbuddy/binaries/python/envs/default/bin/python
$PY scripts/selftest.py

用合成赛道跑通整条流水线并断言模型不变量:GPX 双命名空间解析、短步距离不丢、天气枪时轴长度、完赛时刻钉在目标、路况只改曲线不改终点、seg_factor 只作用于本段、legacy 扁平 profile 兼容、渲染无 NaN/无 JS 报错。全绿才算可以发版。

4b. 无头验证真实产物:

CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
"$CHROME" --headless=new --disable-gpu --no-sandbox --enable-logging=stderr --log-level=0 \
  --virtual-time-budget=8000 --dump-dom "file://$PWD/out/race_plan.html" > /tmp/dom.html 2> /tmp/console.log
  • grep -iE "Uncaught|Error" /tmp/console.log(process_mac.cc 之类的 macOS 进程告警可忽略);
  • 数 <tbody> 内 <tr> 行数(应 = 站点数×2 − 2×端部;若为 0=JS 没跑);
  • 长图截图 --screenshot=/tmp/p.png --window-size=1500,3200,PIL 裁首几行放大复核时刻。

5. 交付

present_files 同时给出 HTML(交互)+ CSV(静态,可用 CSV 按钮或脚本导出)。说明中写清:口径(等效距离公式、爬升口径差异)、关门闸门与各档余量、绕/改道对末段的影响。

模型(脚本已实现,v2)

逐段等效距离(含下降 + 分段强度):
  segEq_i = (段距 + 段D+ / climb_km + 段D− / loss_km_eff) · seg_factor_i
  loss_km_eff = loss_km × 路况系数        # 干地1.0 / 泥泞0.45(泥泞时下降更吃力)
累计等效 eqTotal = Σ segEq_i
后程降速(对等效距离):t(eq) = p0 · (eq + beta·eq²/(2·eqTotal)),p0 = Tmov / (eqTotal·(1+beta/2))
到达_i = 前站出发 + (t(eq_i) − t(eq_{i-1}));出发_i = 到达_i + 停留_i
  • 目标列为含停留总用时(多数用户口径);纯跑动 = 总用时 − 停留合计。
  • 段等效配速 = 段跑动用时 / 段等效距离;段实际配速 = 段跑动用时 / 段实际距离。
  • v2 关键升级:等效距离计入下降(loss_km),且「路况=泥泞」会加大下降成本(落地云丘山 2026 雨战:泥泞技术下坡被严重低估);seg_factor 支持逐段强度。

内置示例与分发

  • examples/yunqiushan50k/ — 云丘山 MMY 50K(GPX 1.0;官方仅公布 3 个关门点;含 weather_history.csv 三年气温样例)。
  • examples/chaigu50k/ — 柴古括苍 50K(GPX 1.1、环形赛道;各检查点关门为按里程比例推算,演示 cutoff_estimated 的「推算」标注;含 weather_history.csv)。
  • examples/moganshan100k/ — 莫干山 100K(调整后赛道;18:00 发枪跨两夜,枪时轴 32 h 真实预报;赛事为雨战故 cond_default:"mud";演示 seg_factor 分段强度与 exposed 风口标注)。
  • 三个示例都内置 weather 块:距比赛日 ≤7 天者走天气预报、>7 天者走气候基准(演示 auto 规则的两种分支)。
  • _icon.png — 技能图标(技能库卡片用;与本 skill 同目录,勿删)。
  • references/submission.md — 提交官方技能库用的简介 / 触发词 / 标签 / 提交说明文案。
  • 打包分发:package_skill.py <skill-dir> [out-dir] → 校验通过后产出 race-plan.zip。

脚本清单

| 脚本 | 作用 | |---|---| | build_course.py | GPX + race_config.json → course.json(吸附、里程锚定、高程剖面、天气) | | build_html.py | course.json → 自包含交互 HTML(含 CSV 导出) | | weatherlib.py | 天气计算库;输出枪时轴 timeline + 钟点轴 profile | | fetch_weather.py | Open-Meteo 免 key 一键拉三年气候基准 + 比赛日预报(联网,跑一次即可固化) | | selftest.py | 离线一键回归:合成数据跑通全流程并断言模型不变量 |

踩过的坑

  • GPX 有 <trkpt> 缺 <ele>(云丘山 6,749 点缺 10 个):用 0.0 兜底会插入「跌到 0 再弹回」的尖刺,50 m 尺度 D+ 会从 ~3,000 m 暴涨到 10 万米级。脚本已做相邻有效点线性插值;断言 gpx_gain / total_gain 落在 0.8–1.6,正常气压 GPX 的 50 m 尺度 D+ 通常比官方高 20%–30%,超 3 倍即数据坏了。
  • 环形赛道终点无独立 waypoint:设 loop:true,F 锚到轨迹末点;脚本用「起终点距离≈0」验证环线闭合。
  • GPX 命名空间:GPX 1.0(…/GPX/1/0)与 1.1(…/GPX/1/1)命名空间不同——用固定命名空间去解析「另一种版本」会静默解析出 0 个 trkpt/waypoint(柴古 GPX 就是 1.1)。脚本已统一 _strip_ns() 剥命名空间,两种版本都能读;若你另写脚本,务必先确认版本。
  • 表格网格:出发行不要 colspan+rowspan 混用(Chrome 会错列);两行都输出完整单元格,出发行元信息列留空只放「↳ 出发」。
  • fmt(minutes):先 Math.round 到整分再拆 H:MM,否则 1919.9999 → "31:00"。
  • 逐段累计:到达时刻的 acc 必须同时累加「本段用时 + 本段停留」(acc += arr + stop)。
  • 内联 payload 与 JS 引用要一致:course.json 缺任一键会让 JS 首句抛错、整表静默为空(只剩标题/控件)。别手抄字段,直接透传;交付前务必验证表格确有行。
  • 关门闸门单独点出:只印一列关门时刻没用——用 banner 明确「最紧的闸门」及其对每档目标的余量。
  • 气温曲线的全局变量勿与图表局部宽变量重名:HTML 模板里图表函数内部有局部 const W = 1200(画布宽);气温用全局 WX(DATA.weather)。若把气温也命名为 W,图表内会命中局部的 1200 → 1200.profile 为 undefined → 气温曲线静默消失(徽章正常、曲线缺失)。改模板时务必检查命名作用域。
  • 气温是「到达时刻」的函数:同一站在不同目标档的到达钟点不同 → 温度也不同;表格按「站×档」逐格插值。海拔高差用 (ele−ref_ele)/100×0.6 °C 修正,避免高海拔点显示成平地温度。
  • 天气全局变量命名:HTML 模板里图表函数内部有局部 const W = 1200(画布宽);气温/降水/风力分别用全局 TW/PR/WN(从 WX.temp/WX.precip/WX.wind 解包),不要把天气变量命名为 W,否则会命中局部的 1200 → 1200.profile 为 undefined → 曲线/徽章静默消失。
  • 降水/风力默认只展示不进模型:与气温一致,二者是「风险提示」,靠面板「路况」开关把雨/泥泞转成对下降成本的放大(loss_km 缩放)。若想让某段单点变难,用 seg_factor 而非改天气。
  • 风是点尺度预报、很粗:山区风口差异大,点尺度风对暴露山脊常偏估;已知风口请在检查点加 exposed:true 让风力徽章带 ⚠,不要盲信数值。
  • 天气必须用「枪时轴」而不是「钟点轴」:百公里跨两夜,若把 24 长度钟点曲线直接按到达小时索引,第二天凌晨的到达会取到前一天同一钟点的雨量——气温看似正常,降水全错。weatherlib 已统一输出 timeline(枪时轴);改模板/读写天气数组时先确认是哪条轴。
  • 距离累加不要丢短步:早期版本为压 GPS 抖动丢弃 ≤1 m 的步进,在密集采样轨迹上(如 91 km / 41k 点)会静默吃掉约 6 km 真实距离,导致高程剖面的里程轴和「GPX 实测」数字整体偏小。抖动交给 50 m 重采样处理,累加时每一步都要算。
  • 天气时间轴是钟点轴,不是里程轴:降水/风力按比赛日整点绘制,各站竖线按「推荐目标到达钟点」定位(与高程剖面里程轴不同),二者别混淆。

实时追踪赛后复盘(2026-10-17 云丘山 MMY 50K · 6740 号实测校准)

用 live 流水线(等效距离 + beta 降速 + 近段惯性/官方三情景)完整追完一场 50K 后的模型校准结论。实测:完赛 13:16:38(21:46:35,关门 22:00,余 14min);分段等效配速 5.9 → 7.7 → 9.7 → 12.8 → 12.9 → 8.7 min/eq·km(U 形)。

模型口径疏忽(做配速表时直接避开):

  • 早期快段污染外推:起跑两段是全程最快(可跑性强),12:41 快照仅凭近段惯性外推 → 预测 17:35,偏差 −4h11m。任何「近段惯性」类外推在 n≤2 时都必须向赛道基准曲线(beta≈0.4 的全程基准,或历史相似段实测)收缩,不能裸用近段配速。
  • 等效距离只计 D+、不计 D−:本场第 3 段(14.5km / D+240 / 净降 554m 长下坡)实际 11.25 min/km,模型按 D+ 折算严重低估其成本。seg_loss 必须进模型:技术下坡段单独设配速带(12–20 min/km),不要并进「100m≈1km」的爬升折算。
  • beta 被分段异质性主导:OLS 拟合 beta 1.5–2.5 全程顶到 clip 上限——真实「降速」不是平滑漂移,而是分段难度结构(大爬升段 vs 收官可跑段)。按 leg 爬升率/下降率归一化后再拟合,或直接分段设配速。
  • 关门冲刺效应缺失:末段 4.3km 从 19.97 → 9.99 min/km(选手为赶 22:00 关门全力冲刺),模型按 12.9 外推高估 20min。距关门 <30min 的赛段要加冲刺 uplift(+10~15% 配速)或至少高亮「需保留冲刺能力」。
  • 官方模型是强基线:本场官方预测误差 −118/−89/+2/+9/+8 min,中后段极准(内置赛道先验)。多情景框架中官方应作一等情景,早期权重上调。
  • 停留假设(已过站实际平均、封顶 20min)本场不是误差源(总停 40.8min),口径可用。

天气/全场性复盘(2026-10-18 补,用全场 35 人样本 + 气象/新闻实况校准):

  • 本场是雨战:连日降雨(10-11 大雨、10-12 中雨、10-16 小转中雨、10-17 小到中雨局部大雨,乡宁气象台 10-17「阴有中雨部分地区大雨 9–19℃ 风 4–5 级」),赛道稀泥,UMY 100M 与 DMY 100K 于 10-17 熔断,MMY 50K 完赛(1276 人完赛 / 170 退赛,完赛率 88%)。此前把 L4/L5 降速归因为「该选手疲劳/U 型」是错的——那是全场共享的条件台阶。
  • 全场对照(35 人样本,各选手按自身均速归一化的分段等效配速):全场中位曲线 0.73 / 0.82 / 0.96 / 1.26 / 1.20 / 0.97(L4 多宝→下川、L5 下川→芦院沟显著变慢);6740 号曲线 0.62 / 0.80 / 1.00 / 1.34 / 1.34 / 0.91,与全场中位相关 0.986——她的「降速形状」几乎就是全场形状,个人特异性只在 L4/L5 慢得更多(分位 86%)。到达 L4 越晚的选手慢得越多(corr≈0.5,与「雨后泥泞随时间累积」一致,但与跑者速度有混淆,只能算佐证)。
  • 对「线性性」的正确回答:L1–L3 基本线性(残差 ≤0.3 min/eq·km);L4 出现 +1.06 min/eq·km 的台阶、L5 回到趋势线附近;该台阶全场共享(中位 +20~26%),不是个体疲劳。单一 beta 框架会把条件台阶误读为选手疲劳(6740 的 OLS beta 1.5–2.5 全是这么来的;全场中位曲线隐含 beta≈1.1,也远高于 clip 0.55——clip 0.55 是按正常天气定的,雨战要翻倍)。
  • 模型改进(已定型为方案):新增「全场相对」情景——用 /races/<id>/progressive?type=FINAL_RANKING&page=N&limit=50(分页必须显式带 type,只带 limit 返回空)+ /runners/<bib> 明细,算实时全场中位分段配速,选手分段配速 ÷ 全场中位 = 条件中性的相对表现,天气/泥泞自动对消。本场官方模型最准(+8min)很可能正是因为内置了全场/赛道先验。
  • 残留混淆(不确定项):L4/L5 台阶在「泥泞湿滑」与「等效距离低估陡坡爬升」(两段都爬到 ~1380m)之间无法用本数据完全分离;新闻侧证据(熔断、满屏泥腿子、悬崖湿滑)支持天气为主因。另:最后一名完赛者 15:34:20 超过名义 14h 关门,疑组委会因天气放宽关门(API 无法确认,待查公告)。

工程疏忽(若做实时追踪版):

  • 终态守卫:选手到达最后一站时剩余赛段为空,投影代码必须走「只记实际成绩」分支,否则 finish=None 直接崩(本场实际发生)。
  • 探测指纹必须与「更新成功」绑定:先落盘新指纹再重算,重算崩溃 → 下次指纹未变 → nochange,终态永远记不下来。崩溃要回滚指纹或标 dirty。
  • 模板 payload 只保留一处实现:本场有两条渲染路径(独立脚本与 pipeline.render_dashboard)各维护一份 payload,两边不同步导致演化图丢「实际完赛」标记。教训:内联数据结构只留一处实现,别在脚本与库里各存一份。
  • 终点前探测间隔:官方对最后一站不再给 nextPointPrediction,兜底逻辑取 15min floor 太粗;剩余 <5km 或预测剩余 <60min 时应缩到 5–10min。

版本历史

2.1.0(2026-10-25)

简化交互,去除易混淆项:

  • 移除「末段冲刺」开关:末段冲刺是模型里的非物理 uplift,使用者容易误解为真实的配速策略,且会改变终点时刻脱离「目标总用时」口径,已整体移除(模型 model() 不再接收 sprint 参数)。
  • 路况只保留「干地 / 泥泞」两档:移除「小雨」中间档(COND_SCALE 仅 {dry:1.0, mud:0.45})。雨天/泥泞统一用「泥泞」表达,避免三档之间的选择困难。降水/风力展示徽章与天气时间轴保持不变(那是天气预报,不是路况)。
  • 其余模型、天气、CSV、示例均不变;旧 course.json 仍向后兼容。

2.0.0(2026-10-22)

在 1.0.0 基础上补上「落雨预测 + 大风预测」并升级模型。

P0 · 天气数据 + 展示

  • weatherlib.py 输出嵌套 temp / precip / wind 三指标,各自支持 climatology / forecast / auto 三态(auto 仍按 ≤7 天切预报)。
  • 到达时刻单元格加 🌧 / 💨 徽章,暴露段加 ⚠。
  • 新增「天气时间轴」:降水柱状 + 气温/风力曲线 + 各站到达竖线 + 官方基线绿「官」刻度。
  • 卡片加「赛程累计降水 / 最大风力 / 最大阵风」,CSV 增列各档气温 / 降水 / 风力。
  • 新增 fetch_weather.py(Open-Meteo 免 key)一键拉取,支持 --start / --hours。

P1 · 路况与等效距离耦合

  • 等效距离计入下降(loss_km,默认 250,即下降按爬升约 2.5 倍折算)。
  • 面板「路况」开关(干地 / 泥泞)按 1.0 / 0.45 缩放下降成本。
  • 切换路况不改变目标总用时,只重塑各段配速曲线(泥泞段占更大权重 → 可跑段需更早「存时间」)。

P2 · 分段强度与官方基线

  • 每站 seg_factor(>1 表示该段更吃力,如长技术下坡 / 泥泞段)。
  • official 官方基线在天气时间轴标绿「官」刻度。
  • 新增 cond_default:"dry" / "mud",预报有雨/赛道泥泞时避免开表即偏乐观。

修掉的真实 bug

  1. 距离累加丢短步(build_course.py):丢弃 ≤1 m 步进,在 91 km / 41k 点轨迹上静默吃掉 6,028 m。改为累加全部步进。
  2. 天气索引用错轴:24 h 钟点曲线按到达小时索引,30 h 百公里第二天凌晨的到达取到前一天同钟点的雨——气温看似正常、降水全错。统一改为枪时轴 timeline。
  3. 轴自动推断过脆:旧规则要求 len(arr) == n_hours,48 h 预报配 30 h 比赛会被误判为日周期。改为长度 ≠ 24 ⇒ 枪时轴(钟点轴必须恰好 24 项),并对「长度 24 且非午夜发枪」的预报发出歧义告警。

兼容性:旧 course.json(扁平 weather.profile)无需重跑即可被新版 HTML 渲染(前端 expand() 自动平铺)。selftest.py 覆盖此回归。

1.0.0

首个可发布版本:GPX → course.json → 自包含交互配速表(等效距离 + beta 后程降速 + 停留 + 关门核对 + 高程剖面 + CSV 导出),含气温因子(近三年同期 / 天气预报自动切换)。

口径核对清单(交付前)

  • [ ] selftest.py 全绿
  • [ ] 终点时刻 == 各目标小时数
  • [ ] 官方里程/关门时刻与手册一致
  • [ ] gpx_gain / total_gain 在 0.8–1.6(否则高程坏了)
  • [ ] 各站 snap < 30 m
  • [ ] 表格确有行(无头 dump 验证)
  • [ ] 天气:weather.n_hours == ceil(max(targets)) + 2,且三指标 timeline 长度一致
  • [ ] 跨夜赛事:抽查第二天凌晨的到达时刻,确认未复用前一天同钟点数值
  • [ ] 泥泞/雨战:cond_default 已设 "mud"