This commit is contained in:
Horldsence
2026-09-13 08:52:23 +08:00
parent dec57e3e29
commit 27f4d6e0d8
26 changed files with 25104 additions and 1 deletions
+71
View File
@@ -0,0 +1,71 @@
# docs 索引
仓库文档分四类:**总览与流程**、**逐问算法**、**验证与对比**、**论文与插图**。
阅读顺序建议按下面的表格从上到下。
## 总览与流程
| 文档 | 内容 |
| --- | --- |
| [`总体思路与流程图.md`](总体思路与流程图.md) | 四问主线、端到端流程图(mermaid)、执行步骤与执行时间速查、交付物与验收锚点 |
| [`执行时间与执行步骤.md`](执行时间与执行步骤.md) | 计时模型、会话状态机与动作回合、附件示例逐步推演、实测日志分解、时间预算与安全阀 |
| [`B题建模分析与实施计划.md`](B题建模分析与实施计划.md) | 四问建模取舍与 P0–P7 实施计划(最早的设计文档) |
| [`协议规则表与状态机.md`](协议规则表与状态机.md) | 指令字段表、结果码、计时模型、状态机(P0 交付) |
## 逐问算法
| 文档 | 内容 |
| --- | --- |
| [`algo-1.md`](algo-1.md) | 问题一实现口径:半平面交、顶点对直径、直径圆充要判定、MEC 清除证书 |
| [`问题一解答.md`](问题一解答.md) | 问题一论文式解答:模型边界、算法可复现步骤、反例构造、演练验证 |
| [`algo-2.md`](algo-2.md) | 问题二:候选集 K1、保证接收盘、安全盘 B(c, 1000−a)、最坏后验选点 |
| [`algo-3.md`](algo-3.md) | 问题三:三层在线架构、七点解析覆盖、覆盖账本、任务调度与停止条件 |
| [`algo-3-bayes.md`](algo-3-bayes.md) | 问题三 `bayes` 策略:网格幸存集滤波、信息增益选点、动态 TSP、Optuna 调参 |
| [`algo-4.md`](algo-4.md) | 问题四:`forward_half_disk` 最小命中集、位置×朝向联合信念、后备盲清 |
## 优化与验证
| 文档 | 内容 |
| --- | --- |
| [`第三四问架构优化与运行提速方案.md`](第三四问架构优化与运行提速方案.md) | 运行历史复核、分层目标架构、P0–P2 优化项与实施路线 |
| [`第三四问图论与动态规划优化方案.md`](第三四问图论与动态规划优化方案.md) | 图论/DP 引入方案与实测回填(含未采用的改动与原因) |
| [`算法审核与优化-20260911.md`](算法审核与优化-20260911.md) | 一次外部口径的算法审核记录与结论 |
| [`最优与基线实现对比.md`](最优与基线实现对比.md) | 1–4 问「最优实现 / 基线实现」对比框架与结果(论文口径说明) |
| [`B题问题3-4相关论文列表.md`](B题问题3-4相关论文列表.md) | 题 3、4 相关文献清单 |
## 论文与插图
| 路径 | 内容 |
| --- | --- |
| [`paper/main.tex`](paper/main.tex) | 论文正文(`latexmk -xelatex main.tex`) |
| [`paper/README.md`](paper/README.md) | 论文编译方式、目录结构与待办标记 |
| [`figures/README.md`](figures/README.md) | 插图清单、配色与字体约定、数据来源与复现命令 |
| [`figures/manifest.json`](figures/manifest.json) | 每张图的数据来源与 sha256、生成命令 |
| [`../project/README.md`](../project/README.md) | 对比框架目录结构与运行方式 |
## 口径登记
同一事实在多份文档中出现时,以下列来源为准;发现不一致请先改文档,再复核图。
| 事实 | 权威来源 |
| --- | --- |
| 题面常量(Ω、速度、各动作耗时、源与频道范围) | `src/mcm26b/config.py`(带 `[题面]` / `[工程]` 标注) |
| 指令字段与结果码 | `src/mcm26b/protocol/rules.py` + [`协议规则表与状态机.md`](协议规则表与状态机.md) |
| 问题一数值(D、ρ、E) | `mcm26b.geometry.solve` 的返回,或 [`问题一解答.md`](问题一解答.md) |
| 覆盖计划与证书 | `src/mcm26b/guarantee/plans/*.json`(`q4_mincover_v2.json` 为默认) |
| 实测虚拟时间 | 行为日志 `virtual_time_s` 字段与 `evaluation.recompute_virtual_time` |
| 冻结基准集对比结果 | `project/results/*.md`(由 `uv run python -m project.run` 生成) |
已知的两处需要随环境更新的引用:
1. [`问题一解答.md`](问题一解答.md) 引用的 `output/q1/figures/*.png` 属于早期
离线插图(`output/` 未入库);现行插图在 [`figures/`](figures/README.md)。
2. 多份文档提到的官方演练报告位于 `output/q3/practice/`、`output/q4/`,
同样未入库;需要时用 `scripts/verify_q3_practice.py` 对留档目录复算。
## 写作约定
- 只写结论与依据,不使用比喻、排比与自评式措辞(例如「唯一权威」「关键洞察」)。
- 数字必须能在代码或数据中找到出处;图内不放标题与解释段落,
说明写在正文,图名与清单见 [`figures/README.md`](figures/README.md)。
- 中文引号统一使用「」,避免与代码中的 ASCII 引号混淆。
+62
View File
@@ -0,0 +1,62 @@
# docs/figures —— 论文插图
每张图只输出 PNG,由 `scripts/make_figures.py` 用 matplotlib 生成。
`manifest.json` 记录每张图的数据来源(路径 + sha256 前 12 位 + 行数)与生成命令。
```bash
uv run python scripts/make_figures.py # 全部 11 张
uv run python scripts/make_figures.py --list # 图名
uv run python scripts/make_figures.py --only geo-q3-seven-point-coverage
uv run python scripts/make_figures.py --scale 1.5 # 整体放大(尺寸倍率)
```
依赖:`matplotlib`、`Pillow`、`docs/paper/fonts/` 下的字体。
`matplotlib` 缓存写入仓库内 `.mpl-cache/`,不污染用户目录。
## 约定
- 图内不放标题、图注与解释性文字,只保留坐标轴、图例与数据标注;
图的含义由文件名与下表承担,解释写在论文正文。
- 配色(`scripts/make_figures.py::C`):深蓝 `#1f4e79` 主流程与协议,
砖红 `#b03a2e` 真值源与关键结论,墨绿 `#1e7a5f` 清除与通过项,
土黄 `#b8860b` 后备与警示,灰色只用于网格与次要文字。
- 字体:中文宋体 `SongtiSC-Regular/Bold`,标题黑体 `HeitiSC-Light/Medium`,
数字与西文 Times New Roman,均取自 `docs/paper/fonts/`,与 `main.tex` 一致。
- 输出为 PNG(`savefig.dpi = 200`,另有 `--scale` 倍率),按 160 mm 宽排版时
与正文同字号。
## 图清单
| 文件 | 内容 | 数据来源 |
| --- | --- | --- |
| `flow-overall-architecture.png` | 总体思路:题面抽象、在线三层、四问主线、三条闭环 | `src/mcm26b/config.py`、`docs/B题建模分析与实施计划.md` |
| `steps-state-machine.png` | 会话状态机(IDLE / 倒计时 / OPEN / CLOSED) | `docs/协议规则表与状态机.md`、`src/mcm26b/protocol/client.py` |
| `timing-attachment-example-105-111-194-199.png` | 附件计时示例逐步推演 | `docs/协议规则表与状态机.md` |
| `timing-measured-q3-10-sources.png` | 实测一局的虚拟时间分解(累计曲线 + 构成) | `referee-data/sessions/BBM9-HZG2-77N5-NKKC/log.jsonl` |
| `geo-q1-convex-intersection-zoom.png` | 问题一:角域交会总览 + 定位区域放大(直径圆与 MEC) | `output/q1/practice/U5ZW-P9KN-TCAG-R7ST_observations.json` |
| `geo-q1-diameter-circle-counterexample.png` | 问题一:直径圆不覆盖的反例(等边三角形布站) | `examples/q1/triangle.json` |
| `geo-q2-guaranteed-reception-disk.png` | 问题二:候选盘、截断角域外接圆、安全盘 `B(c, 1000−a)` | `src/mcm26b/config.py`、`docs/algo-2.md` |
| `geo-q3-seven-point-coverage.png` | 问题三:七点覆盖(7 个 1000 m 接收盘的并集覆盖 Ω)与三套计划对比 | `src/mcm26b/planner/seven.py`、`docs/algo-3.md` |
| `geo-q4-minimum-cover-31-points.png` | 问题四:31 点最小命中集与 `forward_half_disk` 语义 | `src/mcm26b/guarantee/plans/q4_mincover_v2.json` |
| `replay-webui-q3-omnidirectional.png` | WebUI 回放画布复现(全向局,10 源全清) | `referee-data/sessions/BBM9-HZG2-77N5-NKKC/log.jsonl` |
| `replay-webui-q4-directional.png` | WebUI 回放画布复现(定向源局) | `referee-data/sessions/GM56-DFXZ-XKDM-XYV7/log.jsonl` |
后两张对应 WebUI 的预览功能:把回放器画布上的 Ω 与真值源、运动轨迹、
检测示向度射线、清除标记,以及右侧事件流水与底部虚拟时间轴一起导出。
## 数据来源与复现
- `timing-measured-*`、`replay-webui-*` 读评测机行为日志(JSONL,含真值)。
`referee-data/` 被 `.gitignore` 忽略,因此另归档两局到 `examples/replay/`;
没有评测机数据时脚本自动改读归档,图一致。
```bash
uv run python scripts/archive_replay_logs.py --list # 本机有哪些局
uv run python scripts/archive_replay_logs.py <案例编码> # 追加归档
```
- `geo-q1-convex-intersection-zoom.png` 读官方演练留档
`output/q1/practice/*_observations.json`(同样被忽略)。文件缺失时改读
`examples/q1/triangle.json` 反例算例,图仍可用,只是案例不同。
- 问题一的几何量由生产求解器 `mcm26b.geometry.solve` 现算;
问题三、问题四直接读覆盖计划与证书,不手抄数字。
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+192
View File
@@ -0,0 +1,192 @@
{
"generator": "scripts/make_figures.py",
"command": "uv run python scripts/make_figures.py",
"palette": {
"ink": "#1c2430",
"muted": "#5c6673",
"grid": "#e4e9ee",
"primary": "#1f4e79",
"primary_fill": "#dce6f1",
"accent": "#b03a2e",
"accent_fill": "#f6dcd8",
"success": "#1e7a5f",
"success_fill": "#d8ece5",
"warn": "#b8860b",
"warn_fill": "#f6ecd2",
"violet": "#5b4b8a"
},
"fonts": {
"dir": "docs/paper/fonts",
"map": {
"song": "SongtiSC-Regular.ttf",
"song_bold": "SongtiSC-Bold.ttf",
"hei": "HeitiSC-Light.ttf",
"hei_bold": "HeitiSC-Medium.ttf",
"times": "Times New Roman.ttf",
"times_bold": "Times New Roman Bold.ttf"
}
},
"figures": [
{
"stem": "flow-overall-architecture",
"png": "docs/figures/flow-overall-architecture.png",
"scale": 1.0,
"sources": [
{
"path": "src/mcm26b/config.py",
"sha256_12": "c513450dc892",
"lines": 213
},
{
"path": "docs/B题建模分析与实施计划.md",
"sha256_12": "537aaa5790f3",
"lines": 237
}
]
},
{
"stem": "steps-state-machine",
"png": "docs/figures/steps-state-machine.png",
"scale": 1.0,
"sources": [
{
"path": "docs/协议规则表与状态机.md",
"sha256_12": "27c8f7051b26",
"lines": 215
},
{
"path": "src/mcm26b/protocol/client.py",
"sha256_12": "0e9797a85cd7",
"lines": 333
}
]
},
{
"stem": "timing-attachment-example-105-111-194-199",
"png": "docs/figures/timing-attachment-example-105-111-194-199.png",
"scale": 1.0,
"sources": [
{
"path": "docs/协议规则表与状态机.md",
"sha256_12": "27c8f7051b26",
"lines": 215
}
]
},
{
"stem": "geo-q1-convex-intersection-zoom",
"png": "docs/figures/geo-q1-convex-intersection-zoom.png",
"scale": 1.0,
"sources": [
{
"path": "output/q1/practice/U5ZW-P9KN-TCAG-R7ST_observations.json",
"sha256_12": "d2cba13464fd",
"lines": 87
}
]
},
{
"stem": "geo-q1-diameter-circle-counterexample",
"png": "docs/figures/geo-q1-diameter-circle-counterexample.png",
"scale": 1.0,
"sources": [
{
"path": "examples/q1/triangle.json",
"sha256_12": "eeb0c92ae21c",
"lines": 29
}
]
},
{
"stem": "geo-q2-guaranteed-reception-disk",
"png": "docs/figures/geo-q2-guaranteed-reception-disk.png",
"scale": 1.0,
"sources": [
{
"path": "src/mcm26b/config.py",
"sha256_12": "c513450dc892",
"lines": 213
},
{
"path": "docs/algo-2.md",
"sha256_12": "24f3e844deb9",
"lines": 312
}
]
},
{
"stem": "geo-q3-seven-point-coverage",
"png": "docs/figures/geo-q3-seven-point-coverage.png",
"scale": 1.0,
"sources": [
{
"path": "src/mcm26b/planner/seven.py",
"sha256_12": "6ac65bd483f8",
"lines": 113
},
{
"path": "docs/algo-3.md",
"sha256_12": "5638192b8669",
"lines": 456
}
]
},
{
"stem": "geo-q4-minimum-cover-31-points",
"png": "docs/figures/geo-q4-minimum-cover-31-points.png",
"scale": 1.0,
"sources": [
{
"path": "src/mcm26b/guarantee/plans/q4_mincover_v2.json",
"sha256_12": "71e54616ae21",
"lines": 162
},
{
"path": "docs/algo-4.md",
"sha256_12": "1774f8bf85db",
"lines": 485
}
]
},
{
"stem": "timing-measured-q3-10-sources",
"png": "docs/figures/timing-measured-q3-10-sources.png",
"scale": 1.0,
"sources": [
{
"path": "referee-data/sessions/BBM9-HZG2-77N5-NKKC/log.jsonl",
"sha256_12": "5a308e53ed09",
"lines": 133,
"case_code": "BBM9-HZG2-77N5-NKKC",
"module": "q3_practice"
}
]
},
{
"stem": "replay-webui-q3-omnidirectional",
"png": "docs/figures/replay-webui-q3-omnidirectional.png",
"scale": 1.0,
"sources": [
{
"path": "referee-data/sessions/BBM9-HZG2-77N5-NKKC/log.jsonl",
"sha256_12": "5a308e53ed09",
"lines": 133,
"case_code": "BBM9-HZG2-77N5-NKKC"
}
]
},
{
"stem": "replay-webui-q4-directional",
"png": "docs/figures/replay-webui-q4-directional.png",
"scale": 1.0,
"sources": [
{
"path": "referee-data/sessions/GM56-DFXZ-XKDM-XYV7/log.jsonl",
"sha256_12": "f498b2073cdf",
"lines": 421,
"case_code": "GM56-DFXZ-XKDM-XYV7"
}
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+208
View File
@@ -0,0 +1,208 @@
# 几何计算示意图册
> 本册逐图给出坐标口径、关键量与图中各元素的含义。插图位于 [`figures/`](figures/),复现命令:
>
> ```bash
> uv run python scripts/make_figures.py --only geo-q1-convex-intersection-zoom,\
> geo-q1-diameter-circle-counterexample,geo-q2-guaranteed-reception-disk,\
> geo-q3-seven-point-coverage,geo-q4-minimum-cover-31-points
> ```
>
> 图中的数字由脚本现算:问题一调用生产求解器,问题三、四直接读覆盖计划与证书;
> 来源与 sha256 记在 [`figures/manifest.json`](figures/manifest.json)。
统一的坐标与符号约定:
- 世界坐标以 Ω 圆心为原点,$x$ 轴向右、$y$ 轴向上,单位**米**;
- Ω 是半径 $R=1800$ m 的圆盘;源的有效接收半径 $r_{\rm eff}\in[1000,1500]$ m;
- 示向度 $\theta$ 以 $+x$ 轴为 $0°$ 逆时针为正,误差半角 $\delta=1°$;
- 清除半径 $r_{\rm clear}=20$ m;
- "可保证清除" ⟺ 存在一点使整个候选区域落在该点 20 m 内 ⟺ $\rho\le 20$ m(充分)。
---
## 图 1 问题一:角域交会与定位区域
![问题一角域交会](figures/geo-q1-convex-intersection-zoom.png)
**左(总览)**:三个检测点(官方演练案例 `U5ZW-P9KN-TCAG-R7ST`,频道 9)
各自的**前向角域** $W_i=\{p:\angle(p-s_i)-\theta_i\in[-1°,+1°]\}$ 画成窄扇形
(2° 宽,图中放大成可见宽度);三者交集就是定位区域 $P$。检测点相距 600~900 m,
而 $P$ 的直径只有 24 m,所以左图里 $P$ 缩成一个点——这是**必须做局部放大**的原因。
**右(放大,视野 ±31.4 m)**:以最小包围圆圆心为中心:
- 浅蓝扇形 = 三个角域在放大框内的可见部分;三者**重叠成白色区域**= $P$;
- 红色实线多边形 = $P$ 的顶点边界(与左图同一个集合);
- 蓝色虚线圆 = **直径圆**,半径 $D/2=11.9984$ m,圆心是中点 $(-450.634,\ 508.944)$;
- 绿色实线圆 = **最小包围圆(MEC)**,$\rho=12.0686$ m,圆心 $(-449.977,\ 507.953)$。
**关键量(图 (c) 栏)**
| 量 | 值 | 判定 |
| --- | ---: | --- |
| 直径 $D(P)$ | 23.9967 m | 顶点对枚举(凸集上最远点必为顶点) |
| MEC 半径 $\rho$ | 12.0686 m | Welzl 算法 + 支撑点枚举对照 |
| 直径圆最大超出 $E$ | +0.6040 m | $E>0$ ⇒ **直径圆不覆盖** $P$ |
| $\rho\le20$ m | 成立 | ⇒ **存在可保证清除的点**(取 MEC 圆心) |
> 这两个结论不矛盾:题目问"直径圆能否覆盖"(答案:不能,$E>0$)
> 与"能否保证清除"(答案:能,$\rho\le20$ m)是**两个不同的问题**。
> $E$ 的定义:$E=\max_i\lVert v_i-o\rVert-D/2$,$o$ 为直径中点;
> 精确算术下最远顶点对就在圆周上,故 $E\ge0$,$E>0$ 即覆盖失败,
> 达到最大值的顶点就是可独立复核的失败证据。
---
## 图 2 问题一:直径圆不覆盖的反例
![直径圆反例](figures/geo-q1-diameter-circle-counterexample.png)
等边三角形布站($S_1(-1000,0)$、$S_2(540,-866)$、$S_3(520,901)$,示向度
$1°,121°,241°$,误差半角 $1°$)。三条 2° 宽的角域交出一个**等边三角形**
$P$:顶点 $(0,0)$、$(40,0)$、$(20,34.641)$。
- 直径 $D=40.0000$ m,最远点对是底边两端 ⇒ 直径圆圆心 $(20,0)$、半径 20 m;
- 到第三个顶点 $(20,34.641)$ 的距离是 $20\sqrt3\approx34.6410$ m > 20 m,
故**直径圆不覆盖**:$E=+14.6410$ m(图中箭头所指);
- 该反例的最小包围圆半径 $\rho=40/\sqrt3\approx23.0940$ m **> 20 m**,
属于"覆盖失败且无法保证清除"的更坏情形。
> 反例是**符合题设测向条件**的:把真实源放在三角形中心 $(20,20\sqrt3/3)$,
> 三个检测点的真实方位与读数偏差均小于 $1°$,且三个检测距离都小于 1500 m,
> 所以它不是"脱离测向模型的任意图形"。构造与证明见
> [`问题一解答.md`](问题一解答.md) 第六节。
---
## 图 3 问题二:保证接收的候选盘与安全盘
![问题二保证接收盘](figures/geo-q2-guaranteed-reception-disk.png)
**候选集**(首次观测之后可能的位置集合):
$$
K_1=\Omega\ \cap\ W(s_1,\theta_1\pm\delta)\ \cap\ B(s_1,\ 1500),
$$
即"Omega 内、首次角域内、且距离不超过最大接收半径"的三者之交。
**解析安全盘**:截断角域(扇形,半径 $R_{\max}=1500$ m、半角 $\delta=1°$)
的最小包围圆恰为 $B(c,a)$,其中 $c=s_1+a\boldsymbol u$($\boldsymbol u$ 为示向度单位向量),
$$
a=\frac{R_{\max}}{2\cos\delta}=\frac{1500}{2\cos1°}=750.1142\ \text{m}.
$$
于是对安全盘内任一点 $q$、扇形内任一点 $p$:
$$
\lVert q-p\rVert\le a+(1000-a)=1000\ \text{m}
\ \Longrightarrow\ q\ \text{必然收到源的信号},
$$
$$
\boxed{\ \text{安全盘半径}=1000-a=249.8858\ \text{m}\ }
$$
(严格非空;图中绿色实心圆,圆心 $c=(649.62,\ 375.06)$)。
第二检测点就在该盘内选,目标是**最小化最坏保守后验包围半径**,
移动与检测成本作次目标;交会角只用于同分筛选与可解释性说明。
> 图中 $a$ 与 1000 m 两个圆几乎重合是可预期的:$\delta$ 只有 $1°$,
> $a=R_{\max}/(2\cos\delta)$ 与 $R_{\max}/2$ 相差 0.0114%,
> 因此**安全盘的"安全"主要来自角域约束,而不是圆心位置**。
---
## 图 4 问题三:七点解析覆盖
![问题三七点覆盖](figures/geo-q3-seven-point-coverage.png)
**站点集**:原点 + 半径 $a$ 的六等分环,
$S(a)=\{(0,0)\}\cup\{a(\cos k\pi/3,\ \sin k\pi/3):k=0,\dots,5\}$。
**定理(`algo-3.md` §4.2)** 当 $0<a/\sqrt3\le R$ 时
$$
\max_{g\in\Omega}\min_{s\in S(a)}\lVert g-s\rVert
\le D(a):=\max\Bigl\{\frac{a}{\sqrt3},\ \sqrt{R^2+a^2-\sqrt3Ra}\Bigr\}.
$$
代入默认 $a=1159.7$ m、$R=1800$ m:
$$
D(a)=984.5391\ \text{m}<1000\ \text{m}
\quad(\text{几何余量 }15.46\ \text{m}).
$$
**图上看到的就是这个证明**:7 个半径 1000 m 的接收盘(浅绿,裁剪在 Ω 内)的
并集**完全覆盖** Ω,最坏点出现在 $(0,-1800)$(Ω 正下方边界,图中圈出),
到最近站点 $1159.7$ m(正下方环站)的距离恰为 984.54 m。
数值复核(脚本内 20 m 网格):Ω 内 25445 个采样点全部被覆盖,**缺口 0**——
解析上界与数值结果一致到小数点后 3 位。
**代价对比**:七点每轮 7 次检测即可证明"无源",49 点网格要 49 次;
巡回长度 $6a\approx6.96$ km(对照 49 点的 NN 巡回约 36.4 km)。
$a$ 的可行下界 $a_{\min}\approx1122.9558$ m(零余量),默认配置留 15 m 以上余量。
**为什么能"发现即证明"**:某频道若在**全部**站点都测得 `no_signal`,
由覆盖定理知该频道必无源(负观测只能排除该站点 1000 m 圆盘,而 $r_{\rm eff}\ge1000$ m),
于是可以安全销账;所有频道销账 ⇒ 完成证明成立 ⇒ 立即 `/exit`。
---
## 图 5 问题四:定向源的 31 点最小命中集与联合信念
![问题四最小命中集](figures/geo-q4-minimum-cover-31-points.png)
**覆盖语义变了**:定向源只向**朝向 $\pm90°$ 的前向半球**辐射,
所以"某站点附近一定收到"这件事必须同时考虑**源的位置与朝向**:
$$
\text{站点 }s\text{ 能发现源}\iff
\lVert s-\text{pos}\rVert\le r_{\rm eff}\ \wedge\
\angle\bigl(\text{heading},\ s-\text{pos}\bigr)\le90°.
$$
这个语义记作 `forward_half_disk`(图中右侧示意)。
**31 点最小命中集**(`q4_mincover_v2`,图中红点):
| 项 | 值 |
| --- | --- |
| 点数 | 31(离线贪心最小命中集,取代 49 点网格) |
| 设计参数 | $r_{\rm design}=965$ m + 前向角裕量 $5°$ |
| 运行期证书 | 15 m / 72 朝向档,325.62 万个 (位置, 朝向) 采样格,**缺口 0** |
| 最坏命中距离 | 990.051 m |
| 最近邻巡回 | 23.9 km ≈ 4776.8 s 虚拟时间 |
| 旧计划对照 | `q4_mincover_v1` 在 15 m/72 档下有 **566** 个缺口(粗采样过拟合) |
**位置 × 朝向联合信念**:负观测(`no_signal`)在问题四里**只约束朝向**——
它只能排除"朝向正对本站点"的那些朝向档,位置信息几乎不更新。
因此证据层维护网格 × 朝向档的联合分布,可见性判据必须与引擎一致
(距离 $\le r_{\rm eff}$ 且夹角 $\le90°$),并按格内角裕量保守裁剪。
当信念坍缩到有限格时启用**后备盲清**:就近到可疑格 `/clear`,
把盲清次数从 125.7 次/局压到 **4.3 次/局**。
> **本轮修复的正确性缺陷**:旧版 `no_signal` 的朝向裁剪规则反向、且漏计格内角裕量,
> 会删掉真源所在的朝向档。已修复,并由
> `tests/test_joint_belief.py` 的随机观测序列保留性测试固化。
---
## 附:把图接进论文
`docs/paper/main.tex` 目前用表格承载结论,插图按下例插入即可:
```latex
\begin{figure}[htbp]
\centering
\includegraphics[width=0.95\textwidth]{figures/geo-q1-convex-intersection-zoom.png}
\caption{问题一:角域交会得到的定位区域,以及直径圆与最小包围圆的关系。}
\label{fig:geo-q1}
\end{figure}
```
图片按 160 mm 宽排版。图内不含标题与图注,说明写在 `\caption` 与正文。
+178
View File
@@ -0,0 +1,178 @@
# 总体思路与流程图
> 插图:[`figures/flow-overall-architecture.png`](figures/flow-overall-architecture.png)(总体思路与算法主线)、
> [`figures/steps-state-machine.png`](figures/steps-state-machine.png)(执行步骤)、
> [`figures/timing-attachment-example-105-111-194-199.png`](figures/timing-attachment-example-105-111-194-199.png)(附件计时示例)。
> 复现:`uv run python scripts/make_figures.py --only flow-overall-architecture,steps-state-machine,timing-attachment-example-105-111-194-199`
本文给出四问的思路、端到端流程图、执行步骤与执行时间。
算法细节指向 [`algo-1.md`](algo-1.md)~[`algo-4.md`](algo-4.md),
协议细节指向 [`协议规则表与状态机.md`](协议规则表与状态机.md)。
---
## 1. 主线
把题面约束写成可计算的几何集合和计时账本,四问共用一套「证据 — 决策 — 执行」结构,
每问都给出可验证的保证:
| 问 | 保证 |
| --- | --- |
| 1 | 直径由顶点对枚举给出;最小包围圆半径 $\rho\le20$ m 时存在可保证清除的点 |
| 2 | 第二检测点取在解析安全盘 $B(c,1000-a)$(半径 249.8858 m)内,必然收到信号 |
| 3 | 七点计划覆盖整个 Ω(解析上界 984.5391 m < 1000 m),据此可判定频道无源 |
| 4 | 31 点最小命中集在 15 m / 72 朝向档证书下缺口为 0 |
![总体思路与算法主线](figures/flow-overall-architecture.png)
---
## 2. 四层结构
| 层 | 内容 | 作用 |
| --- | --- | --- |
| L0 题面抽象 | Ω 半径 1800 m;移动 5 m/s、检测 5 s、换频 1 s、清除 5 s / 3 s;观测 `direction/no_signal/near`;源 10~16 个,频道 1~20,定向源覆盖 ±90° | 后续推导只用这些量,不引入题面之外的信息 |
| L1 在线三层 | 证据层(候选集与粒子)、决策层(选点与任务表)、执行层(串行客户端) | 依赖单向向下,几何结论与调度启发式可分别测试 |
| L2 四问 | Q1 定位解算 → Q2 第二点选择 → Q3 全向搜索 → Q4 混合定向 | 后一问复用前一问的内核 |
| L3 三条闭环 | 发现、定位、清除 | 每环有明确的进入与退出条件 |
L0 的常量集中在 `src/mcm26b/config.py`,带 `[题面]` / `[工程]` 标注。
证据层只给出「可能在哪里」,决策层只读证据并产出动作,
执行层负责发送动作并维护虚拟时钟。
---
## 3. 四问思路
### 问题一
1. 检测点 $s_i$ 与示向度 $\theta_i$ 给出前向角域
$W_i=\{p:\angle(p-s_i)-\theta_i\in[-1°,+1°]\}$,定位区域是半平面交 $P=\bigcap_i W_i$;
2. 直径 $D(P)=\max_{i,j}\lVert v_i-v_j\rVert$ 只需枚举顶点对(凸集上最远点必为顶点);
3. 直径圆能否覆盖 $P$:唯一候选圆心是最远顶点对的中点。若 $a,b$ 同在该圆内,
三角不等式两处同时取等,圆心只能是 $ab$ 中点,因此只需在该点检查全部顶点;
4. 能否保证清除:最小包围圆半径 $\rho\le20$ m 时,停在 MEC 圆心即可清除。
这是充分条件,不是必要条件。
结论:直径圆不一定覆盖定位区域,见 [`geo-q1-counterexample.png`](figures/geo-q1-diameter-circle-counterexample.png);
官方演练案例是「覆盖失败但 $\rho=12.07$ m $\le20$ m,可保证清除」。
### 问题二
候选集 $K_1=\Omega\cap W(s_1,\theta_1\pm\delta)\cap B(s_1,1500)$。
截断角域(扇形)的最小包围圆为 $B(c,a)$,$a=R_{\max}/(2\cos\delta)=750.1142$ m,
故安全盘 $B(c,1000-a)$ 的半径为 249.8858 m,盘内任一点到扇形内任一点的距离
不超过 1000 m,即必然收到信号。在该盘内以最坏保守后验包围半径为主目标选点,
移动与检测成本作次目标,交会角只用于同分筛选。详见 [`algo-2.md`](algo-2.md)。
### 问题三
发现保证用七点计划(原点 + 半径 $a=1159.7$ m 的六等分环):解析上界
$D(a)=\max\{a/\sqrt3,\ \sqrt{R^2+a^2-\sqrt3Ra}\}=984.5391$ m < 1000 m,
Ω 内任意位置的源都在某站点 1000 m 内。证据层按频道维护候选集与粒子幸存集,
负观测按 `full_disk` 语义排除 1000 m 圆盘;决策层用信息增益选点,
任务表包含检测、定位、清除三类任务,排序为开放路径问题;
所有频道销账即完成证明成立,立即 `/exit`。
### 问题四
定向源只向朝向 ±90° 的前向半球辐射,覆盖语义改为 `forward_half_disk`。
七点在该语义下有缺口,改用离线贪心得到的 31 点最小命中集(设计半径 965 m,
前向角裕量 5°),并以 15 m / 72 朝向档的运行期证书校核(325.62 万个采样格,缺口 0,
最坏命中 990.05 m)。负观测只约束朝向,因此维护位置 × 朝向联合信念,
信念坍缩到有限格时使用后备盲清。详见 [`algo-4.md`](algo-4.md)。
---
## 4. 端到端流程
```mermaid
flowchart TD
S([开始:/enter 成功<br/>现实预算 1200 s 起算]) --> P0[读配置 ProblemConfig]
P0 --> PLAN{按问题选择覆盖计划}
PLAN -->|问题 3| S7[七点全向计划<br/>解析上界 984.54 m]
PLAN -->|问题 4| S31[31 点最小命中集<br/>forward_half_disk 证书]
S7 --> LOOP
S31 --> LOOP
subgraph LOOP[主循环:动作回合]
direction TB
NEXT[选下一个任务<br/>信息增益 / 最坏后验半径<br/>滚动 2-opt 排序] --> SEND[/发送动作/]
SEND --> RESP{accepted?}
RESP -->|false| FIX[不更新状态<br/>记录并重排任务]
RESP -->|true| CLK[更新虚拟时钟、位置、频道]
FIX --> NEXT
CLK --> OBS{观测类型}
OBS -->|direction| HIT[登记新源<br/>同频道安排第二点交会]
OBS -->|no_signal| NEG[负观测裁剪<br/>位置候选或朝向档]
OBS -->|near| SKIP[距离过近,换点重测]
HIT --> DONE{该频道 ρ ≤ 20 m?}
NEG --> DONE
SKIP --> DONE
DONE -->|是| CLR[/清除动作/]
DONE -->|否| NEXT
CLR --> LEDGER[覆盖账本销账]
LEDGER --> FIN{全部频道销账?}
FIN -->|否| NEXT
end
FIN -->|是| EXITQ[完成证明成立,立即 /exit]
EXITQ --> REP[日志复算与统计]
REP --> END([结束:虚拟时间即成绩])
classDef main fill:#dce6f1,stroke:#1f4e79,color:#1c2430
classDef acc fill:#f6dcd8,stroke:#b03a2e,color:#1c2430
classDef ok fill:#d8ece5,stroke:#1e7a5f,color:#1c2430
class NEXT,SEND,RESP,CLK,OBS main
class CLR,FIN,EXITQ acc
class LEDGER,REP ok
```
会话状态机与单次动作回合见 [`figures/steps-state-machine.png`](figures/steps-state-machine.png),
逐步计时示例见 [`figures/timing-attachment-example-105-111-194-199.png`](figures/timing-attachment-example-105-111-194-199.png)。
---
## 5. 执行步骤与执行时间
一次动作回合:本地预校验 → 发送 → 等响应计时 → 按响应更新状态。
异常时同 `request_id` 同内容重发,400 与 409 立即失败。
| 动作 | 虚拟耗时 | 说明 |
| --- | --- | --- |
| `/enter`、`/exit` | 0 s | 不改变虚拟时刻,`/exit` 不切频道 |
| 移动 | 直线距离 / 5 m·s⁻¹ | 同点不动为 0 s |
| 换测向频道 | 1 s | 仅 `/measure` 触发,`/clear` 不切换 |
| 检测 `/measure` | 5 s | 与返回结果无关 |
| 清除 `/clear` | 5 s / 3 s | 成功 5 s,未发现 3 s |
实测样例(评测机日志 `BBM9-HZG2-77N5-NKKC`,问题三演练,10 源全清):
| 项 | 数值 |
| --- | --- |
| 总虚拟时间 | 3134.643 s |
| 移动(里程 11.9 km) | 2379.6 s |
| 检测与清除动作(119 + 10 次) | 645.0 s |
| 换频(110 次) | 110.0 s |
| 现实耗时 | 1.889 s |
三项之和等于响应中的 `virtual_time_s`,与 `evaluation.recompute_virtual_time` 口径一致。
分解图见 [`figures/timing-measured-q3-10-sources.png`](figures/timing-measured-q3-10-sources.png),
定向局(`GM56-…`,虚拟 8703.766 s,现实 3.397 s)与复现命令见
[`执行时间与执行步骤.md`](执行时间与执行步骤.md)。
---
## 6. 交付物与验收锚点
| 问 | 算法文档 | 主要实现 | 锚点 |
| --- | --- | --- | --- |
| 1 | [`algo-1.md`](algo-1.md)、[`问题一解答.md`](问题一解答.md) | `src/mcm26b/geometry/` | 演练复算 $D=23.9967$ m、$\rho=12.0686$ m、$E=+0.6040$ m |
| 2 | [`algo-2.md`](algo-2.md) | `geometry/`、`belief/`、`planner/` | 安全盘半径 249.8858 m |
| 3 | [`algo-3.md`](algo-3.md)、[`algo-3-bayes.md`](algo-3-bayes.md) | `planner/bayes_loop.py`、`planner/seven.py` | 七点上界 984.5391 m < 1000 m |
| 4 | [`algo-4.md`](algo-4.md) | `planner/directional.py`、`belief/joint.py` | 证书缺口 0,最坏命中 990.05 m |
| 1–4 | [`最优与基线实现对比.md`](最优与基线实现对比.md) | `project/` | `uv run python -m project.run` |
优化过程与未采用的改动见
[`第三四问架构优化与运行提速方案.md`](第三四问架构优化与运行提速方案.md)、
[`第三四问图论与动态规划优化方案.md`](第三四问图论与动态规划优化方案.md)。
+178
View File
@@ -0,0 +1,178 @@
# 执行时间与执行步骤
> 配套插图:[`figures/steps-state-machine.png`](figures/steps-state-machine.png)(状态机与动作回合)、
> [`figures/timing-attachment-example-105-111-194-199.png`](figures/timing-attachment-example-105-111-194-199.png)(附件示例逐步推演)、
> [`figures/timing-measured-q3-10-sources.png`](figures/timing-measured-q3-10-sources.png)(实测一局的时序分解)。
> 复现:`uv run python scripts/make_figures.py --only steps-state-machine,timing-attachment-example-105-111-194-199,timing-measured-q3-10-sources`
本文说明每一步做什么、每步花多少虚拟时间、这些数字如何验证。
规则原文见 `issue/` 下附件1 表1、表2 与附件2 第 4、10 节;
可执行表格在 `src/mcm26b/protocol/rules.py` 与 `src/mcm26b/config.py` 的 `CostConfig`。
---
## 1. 计时模型
忽略微秒量化时,一次会话的总虚拟时间为
$$
T=\underbrace{\frac{L}{5}}_{\text{移动}}+\underbrace{5N_m}_{\text{检测}}
+\underbrace{N_s}_{\text{换频}}+\underbrace{5N_c^{+}}_{\text{清除成功}}
+\underbrace{3N_c^{-}}_{\text{清除未发现}},
\qquad L=\text{路径长度(米)},\ 5\ \text{m/s}.
$$
规则:
- 移动按两点间直线距离计,不计加减速;
- 换频只在 `/measure` 触发,任意两频道间固定 1 s,`/clear` 不切换频道;
- 检测固定 5 s,与结果无关,`direction` 与 `no_signal` 同价;
- 清除成功 5 s(光学定位 3 s + 激光清除 2 s),未发现目标 3 s(仅光学定位);
- `/enter`、`/exit` 不产生虚拟时间;
- 访问顺序是开放路径问题,起点固定、终点自由,不必回到起点。
![执行步骤:状态机与动作回合](figures/steps-state-machine.png)
---
## 2. 执行步骤
### 2.1 会话级状态机
| 状态 | 进入条件 | 允许的动作 | 退出条件 |
| --- | --- | --- | --- |
| `IDLE` | 界面待机(演练不限次 / 正式各 3 次) | — | 点击"开始测试" |
| `COUNTDOWN` | 数据准备完成 | —(接口未开放) | 5 s 倒计时结束 |
| `OPEN` | 倒计时结束,接口在 `:2026` 开放 | `/enter` → `/measure` `/clear` → `/exit` | 25 分钟窗口到期 / 源全清后主动 `/exit` / 中止 |
| `CLOSED` | 窗口到期或进程退出 | —(连接关闭,已登记动作可完成) | — |
现实预算与现实窗口是两条独立时钟:`/enter` 成功后 `max_real_duration_s = 1200 s`
起算(程序侧),测试窗口 `test_window_s = 1500 s`(服务端侧)。
客户端维护本地截止时钟,剩余不足 60 s(`reserve_exit_s`,工程参数)时不再发起新动作,
只保证 `/exit` 能发出去。
### 2.2 单次动作回合(严格串行)
![动作回合](figures/steps-state-machine.png)
1. 本地预校验:`channel ∈ [1,20]`、坐标有限且 $\lvert x\rvert,\lvert y\rvert\le 2\times10^6$、
`request_id` 长度与字符集,与服务器 400 语义一致,省一次往返;
2. 串行发送:同一时刻只有一个未决请求,不并发(并发会得到 409);
3. 等服务端计时:`accepted=true` 才更新状态,虚拟时刻取响应中的 `virtual_time_s`,
不做本地累加;`accepted=false` 抛 `ActionRejected`,状态不更新,
`virtual_time_s=0` 不计入时钟;
4. 更新本地状态:位置、当前频道、剩余现实预算,追加 JSONL 会话日志;
5. 异常处理:网络错误、429、500 时同 `request_id` 同内容重发
(请求体一次性序列化,最多 `max_attempts` 次,指数退避);
400、404、405、409、413、415 立即失败。
重发不会重复计时,服务端按 `request_id` 幂等。
### 2.3 任务类型
| 任务 | 触发 | 动作序列 | 虚拟成本 |
| --- | --- | --- | --- |
| 覆盖扫描 | 某频道尚未被任何站点覆盖 | 移动到覆盖点 → `/measure` | 距离/5 + 5 s(+ 换频 1 s) |
| 定位交会 | 某频道收到 `direction` | 移动到第二检测点 → `/measure` | 距离/5 + 5 s(+ 1 s) |
| 清除 | 同频道候选区域 ρ ≤ 20 m | 移动到 MEC 圆心附近 → `/clear` | 距离/5 + 5 s 或 3 s |
| 后备盲清(问题四) | 联合信念坍缩到有限格 | 依次到可疑格 `/clear` | 距离/5 + 3 s 或 5 s |
| 机会测量 | 已停靠且该频道待测 | 原地 `/measure` | 5 s |
任务表排序是开放路径问题,默认用滚动 2-opt 与 Or-opt;
精确 Held-Karp 与路线感知探点在本模拟器上为负收益,默认关闭,见
[`第三四问图论与动态规划优化方案.md`](第三四问图论与动态规划优化方案.md) §8.6。
---
## 3. 附件计时示例逐步推演(105 / 111 / 194 / 199)
| 步 | 指令 | 移动 | 换频 | 动作 | 本步 | 累计 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | `/enter` | 0 | 0 | 0 | 0 | 0 |
| 2 | `/measure (300,400) ch1` | 500 m → 100 s | 0(初始即 1) | 5 | 105 | 105 |
| 3 | `/measure (300,400) ch2` | 0 | 1 | 5 | 6 | 111 |
| 4 | `/clear (300,0) ch3` | 400 m → 80 s | 0 | 3(未发现) | 83 | 194 |
| 5 | `/measure (300,0) ch2` | 0 | 0(第 3 步后已是 2) | 5 | 5 | 199 |
| 6 | `/exit` | 0 | 0 | 0 | 0 | 199 |
![附件计时示例逐步推演](figures/timing-attachment-example-105-111-194-199.png)
验证方式:
```bash
uv run mcm26b sim-check # 客户端 → 本地模拟器链路,输出 [105, 111, 194, 199]
uv run pytest tests/test_protocol_timing.py -q # 逐步断言移动、换频、动作三个分项
```
---
## 4. 实测时序分解
对评测机留档的每一局,`scripts/make_figures.py` 按与 `referee.simulator`
同源的规则逐动作复算,并检查分项之和是否等于响应中的 `virtual_time_s`。
### 4.1 问题三演练局 `BBM9-HZG2-77N5-NKKC`(10 源全清)
| 项 | 数值 | 占比 |
| --- | ---: | ---: |
| 总虚拟时间 | 3134.643 s | 100% |
| 移动(里程 11.9 km) | 2379.6 s | 75.9% |
| 检测与清除动作(119 + 10 次) | 645.0 s | 20.6% |
| 换频(110 次) | 110.0 s | 3.5% |
| 现实耗时 | 1.889 s | — |
### 4.2 问题四演练局 `GM56-DFXZ-XKDM-XYV7`(12 源,含 3 个定向源)
| 项 | 数值 | 占比 |
| --- | ---: | ---: |
| 总虚拟时间 | 8703.766 s | 100% |
| 移动(里程 32.4 km) | 6489.8 s | 74.6% |
| 检测与清除动作(338 + 79 次) | 1951.0 s | 22.4% |
| 换频(263 次) | 263.0 s | 3.0% |
| 现实耗时 | 3.397 s | — |
![实测执行时序分解](figures/timing-measured-q3-10-sources.png)
### 4.3 结论
1. 移动占约 75%,优化重点是路径长度,这解释了七点计划(7 点、6.96 km 巡回)
优于 49 点网格的原因;
2. 动作占约 21%,检测 5 s 是下限,因此找全后立即退出比复扫更省时间;
3. 现实耗时与成绩无关,本地回环为毫秒级,官方口径取虚拟时间,
程序预算(1200 s)只用于限制计算耗时。
### 4.4 复现命令
```bash
# 用评测机留档重画(含真值)
uv run python scripts/make_figures.py --only timing-measured-q3-10-sources,\
replay-webui-q3-omnidirectional,replay-webui-q4-directional
# 归档新日志,评测机数据被清理后仍可复现插图
uv run python scripts/archive_replay_logs.py --list
uv run python scripts/archive_replay_logs.py BBM9-HZG2-77N5-NKKC
# 按官方口径逐微秒复算(接受客户端 JSONL 或评测机 log.jsonl)
uv run python scripts/analyze_run_timing.py referee-data/sessions/BBM9-HZG2-77N5-NKKC/log.jsonl
uv run python scripts/analyze_run_timing.py examples/replay/*.jsonl --json /tmp/timing.json
uv run python scripts/verify_q3_practice.py output/q3/practice/<本局目录>
```
---
## 5. 时间预算与安全阀
| 约束 | 值 | 来源 | 客户端行为 |
| --- | --- | --- | --- |
| 移动速度 | 5 m/s | 题面 | 路径长度决定虚拟时间 |
| 检测 / 换频 | 5 s / 1 s | 题面 | 避免高频扫频 |
| 清除成功 / 未发现 | 5 s / 3 s | 题面 | 盲清代价可控 |
| 测试窗口 | 1500 s(现实) | 题面 | 窗口到期接口关闭 |
| 程序预算 | 1200 s(现实) | 题面 | `/enter` 后本地时钟起算 |
| 退出预留 | 60 s | 工程 | 剩余不足时只允许 `/exit` |
| 虚拟世界限时 | 360000 s | 题面 | 实测最坏为 1e4 量级,未触及 |
| 行为日志上限 | 2 MB | 题面 | 控制请求数 |
安全阀见 [`algo-3.md`](algo-3.md) §7.2 与 [`algo-4.md`](algo-4.md) §5.2:
预算将尽、连续无新发现或联合信念长时间不收敛时,转入最低成本收尾,
先清除并登记已有发现,再 `/exit`,并写入 `stop_reason`。