diff --git a/docs/figures/README.md b/docs/figures/README.md index c0835fb..8059d13 100644 --- a/docs/figures/README.md +++ b/docs/figures/README.md @@ -1,25 +1,22 @@ # docs/figures —— 论文插图 -每张图只输出 PNG。数据由 `scripts/build_figure_data.py` 从行为日志、覆盖计划与生产求解器 -算好写入 `docs/figures/.data/*.mat`,再由 MATLAB(`scripts/matlab/*.m`)排版出图; -`manifest.json` 记录每张图的数据来源(路径 + sha256 前 12 位 + 行数)与生成命令。 +每张图只输出 PNG。正文插图统一由 scripts/plot_paper_figures.py 使用 matplotlib 生成, +数据直接读取 docs/paper/data/formal-results.csv、第四问覆盖计划与论文示例; +manifest.json 记录图片生成命令与来源。 本目录的 11 张 PNG 全部进入论文正文:`docs/paper/main.tex` 用 `\graphicspath{{../figures/}}` 直接引用,不在论文里重绘,也不手抄图内数字。 其中回放画布两张与实测计时一张取自裁判机演练留档, 论文图注均标注「演练、非正式成绩」;问题一的三点交会算例取自演练观测。 -```bash -uv run python scripts/build_figure_data.py # 全部 11 张(数据 + 出图) -uv run python scripts/build_figure_data.py --prepare # 只写 .mat 数据 -uv run python scripts/build_figure_data.py --list # 图名 -uv run python scripts/build_figure_data.py --only geo-q3-seven-point-coverage +python scripts/plot_paper_figures.py # 生成正文全部 matplotlib 插图 +python scripts/plot_paper_figures.py --only geo-q3-seven-point-coverage ``` MATLAB 内手动出图:`cd scripts/matlab` 后执行 `run_all_figures`(每张图单独一个会话, 本机图形子系统连续渲染多张会崩)。 -依赖:`numpy`、`scipy`(写 `.mat`)与 MATLAB R2025a。 +依赖:`numpy` 与 `matplotlib`。 ## 约定 @@ -71,3 +68,5 @@ q4 回放固定用演练局 `GHSN-M38E-74GD-YLUD`(`build_figure_data.py::Q4_RE `examples/q1/triangle.json` 反例算例,图仍可用,只是案例不同。 - 问题一的几何量由生产求解器 `mcm26b.geometry.solve` 现算; 问题三、问题四直接读覆盖计划与证书,不手抄数字。 + + diff --git a/docs/figures/flow-overall-architecture.png b/docs/figures/flow-overall-architecture.png index 3f6621e..19eb953 100644 Binary files a/docs/figures/flow-overall-architecture.png and b/docs/figures/flow-overall-architecture.png differ diff --git a/docs/figures/formal-time-bars.png b/docs/figures/formal-time-bars.png new file mode 100644 index 0000000..e8b231c Binary files /dev/null and b/docs/figures/formal-time-bars.png differ diff --git a/docs/figures/formal-total-time.png b/docs/figures/formal-total-time.png new file mode 100644 index 0000000..1ecd4f8 Binary files /dev/null and b/docs/figures/formal-total-time.png differ diff --git a/docs/figures/framework-decision-loop.png b/docs/figures/framework-decision-loop.png new file mode 100644 index 0000000..c29b910 Binary files /dev/null and b/docs/figures/framework-decision-loop.png differ diff --git a/docs/figures/geo-q1-convex-intersection-zoom.png b/docs/figures/geo-q1-convex-intersection-zoom.png index 1969789..900525e 100644 Binary files a/docs/figures/geo-q1-convex-intersection-zoom.png and b/docs/figures/geo-q1-convex-intersection-zoom.png differ diff --git a/docs/figures/geo-q1-diameter-circle-counterexample.png b/docs/figures/geo-q1-diameter-circle-counterexample.png index df4f953..2ca41b8 100644 Binary files a/docs/figures/geo-q1-diameter-circle-counterexample.png and b/docs/figures/geo-q1-diameter-circle-counterexample.png differ diff --git a/docs/figures/geo-q2-guaranteed-reception-disk.png b/docs/figures/geo-q2-guaranteed-reception-disk.png index 518c8c8..a7fac2a 100644 Binary files a/docs/figures/geo-q2-guaranteed-reception-disk.png and b/docs/figures/geo-q2-guaranteed-reception-disk.png differ diff --git a/docs/figures/geo-q3-seven-point-coverage.png b/docs/figures/geo-q3-seven-point-coverage.png index e626c65..175bce7 100644 Binary files a/docs/figures/geo-q3-seven-point-coverage.png and b/docs/figures/geo-q3-seven-point-coverage.png differ diff --git a/docs/figures/geo-q4-minimum-cover-31-points.png b/docs/figures/geo-q4-minimum-cover-31-points.png index 1d0ddbb..edd808b 100644 Binary files a/docs/figures/geo-q4-minimum-cover-31-points.png and b/docs/figures/geo-q4-minimum-cover-31-points.png differ diff --git a/docs/figures/manifest.json b/docs/figures/manifest.json index bfb9d8d..77b3796 100644 --- a/docs/figures/manifest.json +++ b/docs/figures/manifest.json @@ -1,7 +1,7 @@ { - "generator": "scripts/build_figure_data.py(数据)+ scripts/matlab/*.m(MATLAB 出图)", - "command": "uv run python scripts/build_figure_data.py", - "manual": "MATLAB 中 cd scripts/matlab 后执行 run_all_figures", + "generator": "scripts/plot_paper_figures.py (matplotlib)", + "command": "python scripts/plot_paper_figures.py", + "manual": "Matplotlib PNGs are generated directly; the legacy MATLAB scripts are no longer used by the paper.", "figures": [ { "stem": "flow-overall-architecture", diff --git a/docs/figures/steps-state-machine.png b/docs/figures/steps-state-machine.png index 801628b..47b2be5 100644 Binary files a/docs/figures/steps-state-machine.png and b/docs/figures/steps-state-machine.png differ diff --git a/docs/figures/timing-attachment-example-105-111-194-199.png b/docs/figures/timing-attachment-example-105-111-194-199.png index 28a2b7c..60f5739 100644 Binary files a/docs/figures/timing-attachment-example-105-111-194-199.png and b/docs/figures/timing-attachment-example-105-111-194-199.png differ diff --git a/docs/paper/data/formal-macros.tex b/docs/paper/data/formal-macros.tex index d666aa7..552f063 100644 --- a/docs/paper/data/formal-macros.tex +++ b/docs/paper/data/formal-macros.tex @@ -11,3 +11,6 @@ \newcommand{\FormalMovementMax}{81.38} \newcommand{\FormalRunMin}{2.680} \newcommand{\FormalRunMax}{7.668} +\newcommand{\FormalVirtualTotal}{36737.086002} +\newcommand{\QThreeProgramTotal}{11.300} +\newcommand{\QFourProgramTotal}{19.443} diff --git a/docs/paper/data/formal-q3-rows.tex b/docs/paper/data/formal-q3-rows.tex index ea91686..a7fc156 100644 --- a/docs/paper/data/formal-q3-rows.tex +++ b/docs/paper/data/formal-q3-rows.tex @@ -1,4 +1,4 @@ -% Generated by scripts/audit_paper_formal.py; seconds; half-up rounding. -测试 1 & \texttt{9HU7-UWJR-BPTR-VYAT} & 13 & 238.075 & 5.596 \\ -测试 2 & \texttt{7TCG-C9EC-53VF-HZ5H} & 15 & 232.889 & 3.024 \\ -测试 3 & \texttt{UVUZ-B4MQ-XSUV-HQFP} & 10 & 290.686 & 2.680 \\ +% Generated from docs/paper/data/formal-results.csv; seconds; half-up rounding. +测试 1 & \texttt{9HU7-UWJR-BPTR-VYAT} & 13 & 3094.978 & 5.596 \\ +测试 2 & \texttt{7TCG-C9EC-53VF-HZ5H} & 15 & 3493.335 & 3.024 \\ +测试 3 & \texttt{UVUZ-B4MQ-XSUV-HQFP} & 10 & 2906.863 & 2.680 \\ diff --git a/docs/paper/data/formal-q4-rows.tex b/docs/paper/data/formal-q4-rows.tex index 240d20d..2ad2186 100644 --- a/docs/paper/data/formal-q4-rows.tex +++ b/docs/paper/data/formal-q4-rows.tex @@ -1,4 +1,4 @@ -% Generated by scripts/audit_paper_formal.py; seconds; half-up rounding. -测试 1 & \texttt{VQUM-ETQ5-74PP-ZQ3J} & 13 & 720.171 & 7.668 \\ -测试 2 & \texttt{5K5T-TRWZ-BPR2-ECAV} & 10 & 927.130 & 5.724 \\ -测试 3 & \texttt{K58B-V5VC-E4EF-CRTW} & 14 & 614.884 & 6.051 \\ +% Generated from docs/paper/data/formal-results.csv; seconds; half-up rounding. +测试 1 & \texttt{VQUM-ETQ5-74PP-ZQ3J} & 13 & 9362.226 & 7.668 \\ +测试 2 & \texttt{5K5T-TRWZ-BPR2-ECAV} & 10 & 9271.302 & 5.724 \\ +测试 3 & \texttt{K58B-V5VC-E4EF-CRTW} & 14 & 8608.382 & 6.051 \\ diff --git a/docs/paper/main-updated.pdf b/docs/paper/main-updated.pdf new file mode 100644 index 0000000..81d7c4b Binary files /dev/null and b/docs/paper/main-updated.pdf differ diff --git a/docs/paper/main.tex b/docs/paper/main.tex index 26ad281..8c64ec4 100644 --- a/docs/paper/main.tex +++ b/docs/paper/main.tex @@ -1,8 +1,10 @@ % XeLaTeX; run from docs/paper. Formal tables are generated, not hand copied. \documentclass[zihao=-4,a4paper]{mcm26b-paper} \usepackage{amsthm,tabularx,float,tikz,xurl} +\usepackage{algorithm} +\usepackage{algpseudocode} \usetikzlibrary{arrows.meta,positioning,calc,patterns} -% Raster figures are rendered by scripts/build_figure_data.py + scripts/matlab/*.m +% Raster figures are rendered by scripts/plot_paper_figures.py with matplotlib. % into docs/figures; this file only includes them, it does not redraw them. \graphicspath{{../figures/}} % The paper carries several small tables and figures; let them share pages @@ -31,6 +33,9 @@ \hrule\vspace{5pt}\textbf{#1}\par\vspace{3pt}\hrule\vspace{5pt} }{\vspace{4pt}\hrule\end{minipage}\par\medskip} \renewcommand{\proofname}{证明} +\floatname{algorithm}{算法} +\renewcommand{\algorithmicrequire}{\textbf{输入:}} +\renewcommand{\algorithmicensure}{\textbf{输出:}} % 附录"核心程序代码"的代码排版样式:等宽小字号、行号、长行折行; % 数学符号与 \ { } 通过 literate 映射,避免逐字符转义代码正文。 \lstdefinestyle{corecode}{% @@ -52,7 +57,6 @@ \hypersetup{pdftitle={基于集合估计与覆盖优化的无线电干扰源自动定位清除}, pdfauthor={},pdfsubject={2026年全国大学生数学建模竞赛B题},pdfkeywords={交会定位,最小包围圆,主动测量,覆盖搜索}} \input{data/formal-macros} -\input{data/project-macros} \begin{document} \setcounter{page}{1} \begin{center} @@ -69,13 +73,13 @@ \textbf{针对问题二},为使第二检测点既能接收信号又具有良好横向基线,先由首次示向度与接收半径上界构造候选位置集,并将“对所有候选源位置均保证接收”表示为 $1000$ m 圆盘之交。随后证明截断角域的最小包围半径为 $1500/(2\cos\delta)$;当有效误差半角取 $1.005^\circ$ 时,得到半径约 $249.885$ m 的充分安全候选盘。在线执行时,本文用更紧的凸外包圆替代解析盘,并在其中比较 $37$ 个候选点的最坏后验半径与移动时间,从而兼顾接收保证与定位收缩。 -\textbf{针对问题三},为在源数量未知时同时避免漏检和无效扫描,本文以原点与六个环站组成七点计划;在环半径 $1159.7$ m 时证明最坏覆盖距离不超过 $984.539$ m,使“七站全无信号”成为该频道缺席的解析证据。实际执行不再逐站全频道扫描,而是为每个频道维护网格幸存集,统一融合正负观测:候选集为空即判缺席,并跳过必为无信号的检测。定位点按期望信息增益与移动代价评分,覆盖、定位与清除三类任务统一建表,用最近邻和开放路径 2-opt 滚动重排。三次正式测试分别清除 $13$、$15$、$10$ 个源,平均定位清除时间为 $238.075$、$232.889$、$290.686$ s/个。 +\textbf{针对问题三},为在源数量未知时同时避免漏检和无效扫描,本文以原点与六个环站组成七点计划;在环半径 $1159.7$ m 时证明最坏覆盖距离不超过 $984.539$ m,使“七站全无信号”成为该频道缺席的解析证据。实际执行不再逐站全频道扫描,而是为每个频道维护网格幸存集,统一融合正负观测:候选集为空即判缺席,并跳过必为无信号的检测。定位点按期望信息增益与移动代价评分,覆盖、定位与清除三类任务统一建表,用最近邻和开放路径 2-opt 滚动重排。三次正式测试分别清除 $13$、$15$、$10$ 个源,总虚拟时间为 $3094.978$、$3493.335$、$2906.863$ s,程序运行时间合计 $\QThreeProgramTotal$ s。 -\textbf{针对问题四},由于定向源使“无信号”同时可能来源于距离与朝向,本文将发现条件由位置圆盘覆盖提升为位置与朝向的前向半盘覆盖,采用 $31$ 点最小命中集作为发现计划,并按 $15$ m 位置间距、$72$ 个朝向档运行期校核;同时用逐频道的(位置 $\times$ 朝向)联合幸存集保留遮挡解释。探点先按可见性概率筛选,再取移动最省者;定位停滞时,利用清除与朝向无关的性质,执行 $20$ m 方格、至多 $400$ 格的预算化后备清除。作为备选,本文还由凸组合性质给出 $49$ 点方格方案的连续覆盖证明。三次正式测试分别清除 $13$、$10$、$14$ 个源,平均定位清除时间为 $720.171$、$927.130$、$614.884$ s/个。 +\textbf{针对问题四},由于定向源使“无信号”同时可能来源于距离与朝向,本文将发现条件由位置圆盘覆盖提升为位置与朝向的前向半盘覆盖,采用 $31$ 点最小命中集作为发现计划,并按 $15$ m 位置间距、$72$ 个朝向档运行期校核;同时用逐频道的(位置 $\times$ 朝向)联合幸存集保留遮挡解释。探点先按可见性概率筛选,再取移动最省者;定位停滞时,利用清除与朝向无关的性质,执行 $20$ m 方格、至多 $400$ 格的预算化后备清除。作为备选,本文还由凸组合性质给出 $49$ 点方格方案的连续覆盖证明。三次正式测试分别清除 $13$、$10$、$14$ 个源,总虚拟时间为 $9362.226$、$9271.302$、$8608.382$ s,程序运行时间合计 $\QFourProgramTotal$ s。 -六次正式测试均主动退出,且清除失败计数为零。正式成绩仅取自官方模拟器 \texttt{test} 目录下的六份正式导出包及其匹配统计;由于正式数据未公开源总数,本文只用清除数量、平均定位清除时间与程序运行时间评价,不据此宣称全清率。 +六次正式测试均主动退出,且清除失败计数为零。正式成绩仅取自官方模拟器 \texttt{test} 目录下的六份正式导出包及其匹配统计;由于正式数据未公开源总数,本文以清除数量、总虚拟时间与程序运行时间评价,不据此宣称全清率。 -\textbf{对比实验}采用仓库内的配对基准集复核上述方法。问题一朴素基线在随机算例上直径平均绝对误差为 $\ProjQOneRandMaeBase$ m、覆盖判定准确率为 $\ProjQOneRandCoverBase$、最大误差为 $\ProjQOneRandMaeMaxBase$ m;问题二忽略接收保证的基线保证接收率为 $\ProjQTwoGuaranteePerp$,只保证接收而缺少横向基线的基线最坏后验半径为 $\ProjQTwoWorstMec$ m;问题三默认策略相对 $49$ 点网格的平均虚拟时间下降 $\ProjQThreeDropOpt$;问题四直接搬用问题三策略时出现 $\ProjQFourAbsentBase$ 次缺席误判。该组数据来自自建冻结基准集与演练局留档,只用于机制说明和相对行为比较,与正式成绩分节陈述,不合并统计。 +\textbf{直接指标}部分只采用三次正式测试的原始统计。问题三三次测试分别清除 $13$、$15$、$10$ 个源,总虚拟时间为 $3094.978$、$3493.335$、$2906.863$ s,合计 $\QThreeTotal$ s,程序运行时间合计 $\QThreeProgramTotal$ s;问题四三次测试分别清除 $13$、$10$、$14$ 个源,总虚拟时间为 $9362.226$、$9271.302$、$8608.382$ s,合计 $\QFourTotal$ s,程序运行时间合计 $\QFourProgramTotal$ s。六次测试均主动退出、清除失败为零,累计清除 $\FormalClearCount$ 个源,总虚拟时间 $\FormalVirtualTotal$ s。由于题目关心任务完成时间和程序运行时间,本文直接以总虚拟时间、程序运行时间和清除数量作为优化效果指标,不使用自建基准集推断相对倍数。 四问的求解核心已整理为约 $5000$ 行的单文件程序,并按四问主线在附录中列出核心函数及一句话说明;评测机、命令行外壳、采集与绘图脚本、测试以及未被采用的方案不计入正文。综合上述,结论可归纳为:1. 带误差交会定位的严格清除判据是最小包围圆半径,而非定位区域直径;2. 主动测量应先保证接收,再在安全区域内优化横向基线与后验收缩;3. 多源搜索需把覆盖证明、正负观测滤波和滚动任务排序统一到同一时间目标下;4. 定向场景必须保留位置与朝向的联合解释,并以有限后备清除兜底。 @@ -141,19 +145,7 @@ \begin{figure}[htbp] \centering -\begin{tikzpicture}[font=\small,>=Stealth, - box/.style={draw=inkblue,rounded corners=2pt,align=center,text width=3.6cm, - minimum height=1.05cm,fill=inkblue!4}] - \node[box] (obs) {接口观测\\示向度/近距离/无信号}; - \node[box,right=1cm of obs] (belief) {候选集更新\\几何外包/网格/朝向}; - \node[box,below=0.85cm of belief] (decision) {选择下一任务\\覆盖/定位/清除}; - \node[box,left=1cm of decision] (act) {执行一个动作\\移动、计时与状态更新}; - \draw[->] (obs)--(belief); \draw[->] (belief)--(decision); - \draw[->] (decision)--(act); \draw[->] (act)--(obs); - \node[align=center,right=0.5cm of decision,text width=2.5cm] (stop) - {满足终止条件\\或预算到限\\$\Downarrow$\\退出并报告}; - \draw[->] (decision)--(stop); -\end{tikzpicture} +\includegraphics[width=0.88\linewidth]{framework-decision-loop.png} \caption{基于观测更新、集合估计与任务排序的闭环决策过程。} \label{fig:framework} \end{figure} @@ -208,7 +200,6 @@ N_m,N_s,N_c^+,N_c^- & 检测、换频、成功清除、失败清除次数 & 次\ \input{sections/q3} \input{sections/q4} \input{sections/results} -\input{sections/comparison} \input{sections/evaluation} \begin{thebibliography}{99} \small\setlength{\itemsep}{1pt} @@ -233,3 +224,4 @@ Mathematics of Operations Research, 1979, 4(3): 233--235. \ctexset{section={number=\Alph{section},aftername={\quad}}} \input{sections/appendix} \end{document} + diff --git a/docs/paper/sections/appendix.tex b/docs/paper/sections/appendix.tex index d907c6d..852e9f9 100644 --- a/docs/paper/sections/appendix.tex +++ b/docs/paper/sections/appendix.tex @@ -16,14 +16,7 @@ python scripts/audit_paper_formal.py --snapshot docs/paper/data/formal-results.j 第二条根据已有快照重新计算指标,并核对现有正式包的校验值。 耗时图与汇总量由同一统计快照生成。 -第~\ref{sec:comparison} 节的配对对比表由 \pathtext{scripts/build_paper_support.py} -从 \pathtext{project/results/*.json} 生成,复跑对比框架本身执行 -\texttt{uv run python -m project.run}。 -正文插图由 \pathtext{scripts/build_figure_data.py} 准备数据、 -\pathtext{scripts/matlab/run_all_figures.m} 出图,逐图来源与生成命令见 -\pathtext{docs/figures/manifest.json}。 -其中回放画布与实测计时两张插图取自评测机演练留档, -只用于说明机制与时间构成,不参与正式统计。 +正文插图统一由 \pathtext{scripts/plot_paper_figures.py} 使用 matplotlib 生成,数据只读取 \pathtext{docs/paper/data/formal-results.csv} 与第四问覆盖计划;逐图来源与生成命令见 \pathtext{docs/figures/manifest.json}。 \section{支撑材料与运行方法} \begin{table}[H] @@ -35,13 +28,13 @@ python scripts/audit_paper_formal.py --snapshot docs/paper/data/formal-results.j \midrule \pathtext{src/mcm26b/} & 四问几何、信念、覆盖、调度与串行通信实现\\ \pathtext{src/mcm26b/guarantee/plans/} & 第四问已实现的覆盖计划及验证参数\\ -\pathtext{project/} & 最优与基线配对对比框架及其冻结基准集结果\\ +\pathtext{scripts/plot_paper_figures.py} & 正文插图 matplotlib 生成脚本\\ \pathtext{scripts/audit_paper_formal.py} & 正式文件对应核对、匿名快照及表格复算\\ -\pathtext{scripts/build_paper_support.py} & 对比表、矢量图、源码附录与支撑包生成\\ -\pathtext{scripts/build_figure_data.py}、\pathtext{scripts/matlab/} & 插图数据准备与 MATLAB 出图脚本\\ +\pathtext{scripts/build_figure_data.py} & 旧插图数据准备脚本,仅作历史保留\\ +\pathtext{docs/figures/manifest.json} & 插图来源与生成命令清单\\ \pathtext{docs/paper/data/} & 六个正式案例的可读统计、表格及校验值\\ \pathtext{docs/paper/sections/} & 分章论文源文件\\ -\pathtext{docs/paper/figures/} & 正式累计耗时图的矢量源文件\\ +\pathtext{docs/paper/figures/} & 旧版矢量图源文件,正文不再引用\\ \pathtext{docs/figures/} & 正文插图(PNG)及其数据清单与生成命令\\ \pathtext{Jammers-simulator-full/test/} & 六份原名官方正式日志\\ \pathtext{support-manifest.json} & 支撑包文件清单及逐文件 SHA-256\\ @@ -65,3 +58,4 @@ Python 运行时需要 \texttt{requests},\texttt{numpy} 提供可选加速。 未收录的部分包括独立评测机(按附件协议复现模拟器,与求解代码零耦合)、 命令行入口、数据采集与论文绘图脚本、单元测试,以及经实测未采用的候选方案。 \input{data/source-listings} + diff --git a/docs/paper/sections/comparison.tex b/docs/paper/sections/comparison.tex index b15deb8..51c8d51 100644 --- a/docs/paper/sections/comparison.tex +++ b/docs/paper/sections/comparison.tex @@ -1,174 +1,2 @@ -\section{算法对比实验:最优实现与基线}\label{sec:comparison} -\subsection{对比口径与有效性判定} -本节数据来自仓库内的配对对比框架 \pathtext{project/}, -用于回答上述几何判据与闭环策略相对朴素做法改进在哪里; -它与第~\ref{sec:formal} 节的六份官方正式导出完全分开,两类数字不合并、不相加。 -框架设三种角色:\textbf{最优实现}直接复用 \pathtext{src/mcm26b} 中已由测试固化的生产代码, -只增加输入输出适配;\textbf{基线实现}是同题下更朴素但能独立跑通的对照, -不共享最优实现的关键步骤;\textbf{独立参考}与分析算法正交,用于判定谁对谁错。 -问题三、四在内存回环评测机上运行,与官方接口使用同一请求与响应边界: -源真值只在评测机一侧,策略端只读响应,因此全清率与缺席误判可以核查。 -一局有效必须同时满足全清、无缺席误判、正常 \texttt{/exit}、零异常与逐微秒计时复算一致, -否则记为无效,失败样本不静默丢弃;所有对比按场景配对,并给出均值、中位与最大值。 -源位置均匀、误差场逐点固定等分布属于自建测试假设,题面并未规定, -本节结论只描述各方法的相对行为。 - -\begin{table}[htbp] -\centering\small -\caption{四问最优实现与基线的核心指标总览(自建基准集)} -\begin{tabularx}{\linewidth}{c X l l} -\toprule -问 & 核心指标 & 最优实现 & 基线实现\\ -\midrule -\csname @@input\endcsname data/project-overview-rows.tex -\bottomrule -\end{tabularx} -\label{tab:projoverview} -\end{table} -表~\ref{tab:projoverview} 中问题二、四的基线分别取“候选圆心”与 -“问题三策略直接搬用”,它们是各自问中最需要区分的对照, -其余各表的配对数字同样如此选择。 -本节全部数字由 \pathtext{scripts/build_paper_support.py} 从 -\pathtext{project/results/*.json} 生成,正文与表格均不手抄。 - -\subsection{问题一:直径与覆盖判定} -\begin{table}[htbp] -\centering\small -\caption{问题一固定示例的直径与覆盖判定(单位:m,$\Delta D$ 为相对参考解的误差)} -\setlength{\tabcolsep}{4.5pt} -\begin{tabular}{lrrrrccc} -\toprule -算例 & $D$参考 & $D$最优 & $D$基线 & $|\Delta D|$基线 & 覆盖参考 & 覆盖最优 & 覆盖基线\\ -\midrule -\csname @@input\endcsname data/project-q1-example-rows.tex -\bottomrule -\end{tabular} -\label{tab:projq1case} -\end{table} - -\begin{table}[htbp] -\centering\small -\caption{问题一的汇总指标(直径误差单位:m,耗时单位:ms)} -\setlength{\tabcolsep}{5pt} -\begin{tabular}{lrrrrr} -\toprule -数据子集/方法 & 状态判定 & 直径平均误差 & 直径最大误差 & 覆盖判定 & 平均耗时\\ -\midrule -\csname @@input\endcsname data/project-q1-summary-rows.tex -\bottomrule -\end{tabular} -\label{tab:projq1sum} -\end{table} -表~\ref{tab:projq1case} 显示基线误差的来源是代理方式而非数值精度: -它以轴对齐包围盒的对角线代替区域直径,把边长 $40$ m 的等边三角形判成 $50$ m, -把零误差退化线段判成 $0$ m;覆盖判定以“包围盒中心加代理直径圆”代替逐顶点检查, -于是对任意包围盒几乎恒判覆盖。 -在 $\ProjQOneExampleCases$ 个固定示例上,基线的状态判定准确率为 $\ProjQOneFixedStatusBase$, -平均直径误差 $\ProjQOneFixedMaeBase$ m,最大误差 $\ProjQOneFixedMaeMaxBase$ m, -覆盖判定准确率仅 $\ProjQOneFixedCoverBase$; -在 $\ProjQOneRandomCases$ 个随机交会算例上状态判定回到 $\ProjQOneRandStatusBase$, -但平均直径误差仍为 $\ProjQOneRandMaeBase$ m,最大误差 $\ProjQOneRandMaeMaxBase$ m, -覆盖判定准确率 $\ProjQOneRandCoverBase$。 -最优实现与独立参考解在全部算例上的状态、直径与覆盖判定一致; -平均耗时为 $\ProjQOneRandMsOpt$ 与 $\ProjQOneRandMsBase$ ms(随机算例), -说明精确几何并未以计算量为代价。 - -\subsection{问题二:第二检测点选择} -\begin{table}[htbp] -\centering\small -\caption{问题二各方法在 $\ProjQTwoCases$ 个随机首次示向度算例上的汇总 -(半径与收缩单位:m,移动时间单位:s)} -\setlength{\tabcolsep}{5pt} -\begin{tabular}{lrrrrrr} -\toprule -方法 & 保证接收率 & 最坏后验均值 & 中位 & 平均收缩 & 终值比 & 移动均值\\ -\midrule -\csname @@input\endcsname data/project-q2-rows.tex -\bottomrule -\end{tabular} -\label{tab:projq2} -\end{table} -表~\ref{tab:projq2} 的算例由随机首次示向度生成, -首次候选集的平均包围半径为 $\ProjQTwoInitialRadius$ m。 -垂直 $1000$ m 的基线交会角最大,却忽略接收保证,保证接收率为 $\ProjQTwoGuaranteePerp$: -真源位于角域远端时第二点必然无信号,其最坏后验均值 $\ProjQTwoWorstPerp$ m -是在丢失样本上算出的,不能与其它方法直接比较。 -候选圆心基线保证接收($\ProjQTwoGuaranteeMec$),但不增加横向基线, -最坏后验半径均值仍为 $\ProjQTwoWorstMec$ m,终值比 $\ProjQTwoRatioMec$,只收缩一半。 -默认选点器在保证接收下取得最坏后验均值 $\ProjQTwoWorstOpt$ m、终值比 $\ProjQTwoRatioOpt$, -与保证接收盘内 $9\times9$ 网格枚举的参考解($\ProjQTwoWorstRef$ m)相差约 $2.3\%$, -说明 $37$ 点环形候选已接近该目标函数下的网格最优。 -这里的最坏后验是对候选外包多边形的采样评估,不是连续极小极大的全局上界; -“保证接收”一项由安全盘独立成立。 - -\subsection{问题三:全向闭环} -\begin{table}[htbp] -\centering\footnotesize -\caption{问题三三种方法在 $\ProjQThreeScenes$ 个冻结场景上的汇总 -(时间单位:s,位移单位:km)} -\setlength{\tabcolsep}{3.5pt} -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrrrrrr} -\toprule -方法 & 有效局 & 全清率 & 缺席误判 & $T$均值/s & $T$中位/s & $T$最大/s & 检测 & 位移/km & 后备清除\\ -\midrule -\csname @@input\endcsname data/project-q3-rows.tex -\bottomrule -\end{tabular*} -\label{tab:projq3} -\end{table} -表~\ref{tab:projq3} 汇总三种方法。它们在 $\ProjQThreeScenes$ 个冻结场景 -(源数 $10$、$13$、$16$,种子与源数版本化) -上全部完成全清,缺席误判均为零,差别只在时间: -默认 \texttt{bayes} 平均 $\ProjQThreeMeanOpt$ s, -七点批量 \texttt{seven} $\ProjQThreeMeanSeven$ s,$49$ 点网格 \texttt{grid} $\ProjQThreeMeanBase$ s。 -按场景配对,\texttt{bayes} 相对 \texttt{grid} 的平均虚拟时间下降 $\ProjQThreeDropOpt$、 -中位下降 $\ProjQThreeDropMedianOpt$,在 $\ProjQThreeFasterOpt$ 场景上更快, -\texttt{seven} 相对 \texttt{grid} 下降 $\ProjQThreeDropSeven$。 -分项上,默认策略检测 $\ProjQThreeMeasuresOpt$ 次、位移 $\ProjQThreeMoveOpt$ km, -网格基线需要 $\ProjQThreeMeasuresBase$ 次检测、$\ProjQThreeMoveBase$ km 位移; -单局在线计算均在 $1.6$ s 以内(默认策略 $\ProjQThreeWallOpt$ s),不构成瓶颈。 -七点方案用解析覆盖保证发现,站点最少,但按站点批量扫频需要绕行,位移约为默认策略的两倍。 -可见三者的差别不是“能否完成”,而是覆盖证明与信息增益选点各自省下了多少移动与检测。 - -\subsection{问题四:混合源闭环} -\begin{table}[htbp] -\centering\footnotesize -\caption{问题四三种方法在 $\ProjQFourScenes$ 个场景上的汇总 -(时间单位:s,位移单位:km)} -\setlength{\tabcolsep}{3.5pt} -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrrrrrr} -\toprule -方法 & 有效局 & 全清率 & 缺席误判 & $T$均值/s & $T$中位/s & $T$最大/s & 检测 & 位移/km & 后备清除\\ -\midrule -\csname @@input\endcsname data/project-q4-rows.tex -\bottomrule -\end{tabular*} -\label{tab:projq4} -\end{table} -表~\ref{tab:projq4} 汇总三种方法。$\ProjQFourScenes$ 个场景取自 core 与 boundary 两组, -boundary 全部是背离圆心发射的边界定向源,属最不利工况。 -\texttt{directional} 在 $\ProjQFourValidOpt$ 局上全部有效,全清率 $\ProjQFourClearOpt$, -平均后备清除 $\ProjQFourFallbackOpt$ 次,对应遮挡导致的定位停滞。 -把问题三的 \texttt{bayes} 直接搬用时全清率降到 $\ProjQFourClearBase$, -并出现 $\ProjQFourAbsentBase$ 次缺席误判:七站全向缺席证明把朝外发射的定向源判为不存在, -因此它用时最短($\ProjQFourMeanBase$ s),但结论是错的。 -\texttt{grid} 的 $49$ 点覆盖对定向源发现完备,全清率 $\ProjQFourClearGrid$、 -平均虚拟时间 $\ProjQFourMeanGrid$ s,却没有朝向推理与后备清除, -定位停滞时只能停在“已发现”状态,$\ProjQFourValidGrid$ 局通过有效性判定。 -\texttt{directional} 平均 $\ProjQFourMeanOpt$ s,比 \texttt{bayes} 高 $\ProjQFourRiseOpt$, -位移 $\ProjQFourMoveOpt$ km 对 $\ProjQFourMoveBase$ km;由于基线 $11$ 局全部无效, -这一比值不构成“默认策略更慢”的证据,在定向源上应先保证判对、再比较快慢。 - -\subsection{对比实验的结论与边界} -四组配对比较支持的结论是:问题一的误差来自代理近似,改用精确顶点枚举与最小包围圆后消失; -问题二要求同时满足接收保证与横向基线,只取其一都会被表~\ref{tab:projq2} 的某一列暴露; -问题三的三种方法都能全清,默认策略的收益来自信息增益选点与滚动排序而非覆盖证明本身; -问题四的差别在正确性,搬用全向缺席证明会“快而错”, -只扩大覆盖而不补朝向推理与后备清除则“稳而不全”。 - -边界有四条。随机分布是自建测试假设,场景数、源数与种子见 -\pathtext{project/results/*.json},不能据此推断官方测试的分布。 -基线不是稻草人:候选圆心、七点批量与网格覆盖都能独立跑通并在部分场景有效, -它们暴露的是各自缺少的那一步。 -问题二的最坏后验与问题四的可见性评分都是启发式评估量,不是全局最优值或校准概率。 -本节指标与第~\ref{sec:formal} 节的官方统计口径不同,两组数字不可互相换算。 +% This section is intentionally not input by main.tex. +% Formal direct metrics are reported in sections/results.tex. diff --git a/docs/paper/sections/evaluation.tex b/docs/paper/sections/evaluation.tex index ed1c03f..276113d 100644 --- a/docs/paper/sections/evaluation.tex +++ b/docs/paper/sections/evaluation.tex @@ -59,12 +59,7 @@ $990.051+15/\sqrt2\approx1000.658$ m,超过最小接收半径; 未来可在可重复场景中开展配对试验, 同时保存策略版本、源数量和轨迹等信息,以估计置信区间并比较各模块的贡献。 -\textbf{第六,配对对比依赖自建基准集。} -第~\ref{sec:comparison} 节的最优与基线对比运行在内存回环评测机上, -源位置均匀、误差场逐点固定等分布是测试假设,题面并未规定; -场景数有限,基线在部分场景同样有效。 -该节结论说明的是各方法缺少哪一步,而不是普适的性能排序, -也不能与官方正式统计互相换算。 +\textbf{第六,正式测试样本仍有限。}本文仅使用问题三、四各三次正式测试的清除数量、总虚拟时间和程序运行时间作为优化效果的直接指标;由于正式数据未公开源总数与类型组成,不能据此计算全清率或做跨问题性能排名。后续应在可重复环境中扩大配对测试,并分别报告总虚拟时间、程序运行时间和清除数量。 \subsection{参数影响的解析讨论} 主要参数的影响可由模型公式分析。 diff --git a/docs/paper/sections/q1.tex b/docs/paper/sections/q1.tex index 1da7b9f..f3c72e5 100644 --- a/docs/paper/sections/q1.tex +++ b/docs/paper/sections/q1.tex @@ -151,18 +151,27 @@ m\in\arg\min_z\max_{g\in P}\norm{g-z}. $D(P)\le40$ 是必要条件,但并非充分条件。 \subsection{算法一及复杂度} -\begin{algorithmblock}{算法1:有界测向角域交会与覆盖圆判定(对应 algo-1)} -\textbf{输入:}同一频道的检测点、示向度和误差半角。\\ -\textbf{输出:}区域状态、顶点、直径、直径圆判定与最小包围圆。 -\begin{enumerate}[leftmargin=2em,itemsep=1pt,topsep=4pt] - \item 若观测为空,返回无界;否则归一化坐标,生成 $3n$ 条半平面。 - \item 枚举非平行边界交点,仅保留满足全部约束的点。 - \item 无可行点时返回空集;存在非零衰退切向时返回无界及无穷直径。 - \item 对可行交点去重、求凸包,并区分单点、线段和二维多边形。 - \item 枚举最远顶点对,计算 $D$、中点 $o$,检查全部顶点距 $o$ 是否不超过 $D/2$。 - \item 求最小包围圆,复核包含关系与数值余量,判断是否满足清除条件。 -\end{enumerate} -\end{algorithmblock} +\begin{algorithm}[htbp] +\caption{有界测向角域交会与覆盖圆判定} +\label{alg:q1} +\begin{algorithmic}[1] +\Require 同一频道的检测点、示向度和误差半角 +\Ensure 区域状态、顶点、直径、直径圆判定与最小包围圆 +\If{观测为空} + \State \Return 无界 +\EndIf +\State 归一化坐标,生成 $3n$ 条半平面约束 +\State 枚举非平行边界交点,仅保留满足全部约束的点 +\If{无可行点} + \State \Return 空集 +\EndIf +\State 检查衰退锥;若存在非零方向,\Return 无界及无穷直径 +\State 对可行交点去重并求凸包,区分单点、线段和二维多边形 +\State 枚举最远顶点对,计算 $D$ 与中点 $o$,检查全部顶点是否在 $\B(o,D/2)$ 内 +\State 计算最小包围圆 $\B(m,\rho)$,复核包含关系与数值余量 +\State \Return $\rho+\eta_{\mathrm{num}}\le20$ 是否成立 +\end{algorithmic} +\end{algorithm} 边界交点枚举需 $O(k^2)$ 次,每点校验 $k$ 条约束,主计算量为 $O(k^3)$; 凸包最坏需 $O(k^2\log k)$,直径枚举需 $O(m^2)$。 Welzl 算法在随机排列下具有期望线性时间。该求解流程适合少量观测, diff --git a/docs/paper/sections/q2.tex b/docs/paper/sections/q2.tex index 520c897..2c263c3 100644 --- a/docs/paper/sections/q2.tex +++ b/docs/paper/sections/q2.tex @@ -141,19 +141,32 @@ $\operatorname{bearing}(q,g)\pm\delta_{\mathrm{eff}}$, $\widehat J$ 是最坏后验半径的\textbf{离散近似}, 用于比较有限候选点的定位效果;接收条件由式~\eqref{eq:safe-disk} 确定。 -\begin{algorithmblock}{算法2:保证接收的主动选点(对应 algo-2)} -\textbf{输入:}首次测点与示向度,或任意已更新的候选凸外包;当前位置。\\ -\textbf{输出:}下一检测点、采样后验评分及接收保证状态。 -\begin{enumerate}[leftmargin=2em,itemsep=1pt,topsep=4pt] - \item 初次调用时构造式~\eqref{eq:k1} 的外包多边形;后续调用使用已有外包。 - \item 计算最小包围圆和安全盘半径 $b=1000-\rho_{\mathrm{out}}-10^{-3}$。 - \item 若 $b<0$,在候选区域圆心追加观测;否则生成 $37$ 个安全候选点。 - \item 对每点枚举边界样本与方向端点读数,补入近距离分支,得到 $\widehat J$。 - \item 按后验评分优先、移动时间次优选点,并以 $15$ m 采样间距复核。 - \item 获得新观测后更新候选集;$\near$ 可就地清除, - $\direction$ 则根据最小包围半径决定清除或追加测量。 -\end{enumerate} -\end{algorithmblock} +\begin{algorithm}[htbp] +\caption{保证接收的第二检测点选择} +\label{alg:q2} +\begin{algorithmic}[1] +\Require 首次测点与示向度,或已更新的候选凸外包;当前位置 +\Ensure 下一检测点、采样后验评分与接收保证状态 +\State 初次调用时构造 $K_1$ 的外包多边形;后续调用沿用已有外包 +\State 计算最小包围圆 $\B(m,\rho_{\mathrm{out}})$ 与安全盘半径 $b=1000-\rho_{\mathrm{out}}-10^{-3}$ +\If{$b<0$} + \State 在候选区域圆心追加观测 +\Else + \State 生成 $37$ 个安全候选点 + \For{每个候选点 $q$} + \State 枚举外包边界样本与方向端点读数,补入近距离分支 + \State 计算最坏后验半径 $\widehat J(q)$ + \EndFor + \State 按后验评分优先、移动时间次优选点,并以 $15$ m 采样间距复核 +\EndIf +\State 获得新观测后更新候选集 +\If{观测为 $\near$} + \State 就地清除 +\Else + \State 根据最小包围半径决定清除或追加测量 +\EndIf +\end{algorithmic} +\end{algorithm} 若候选数为 $N_q$、边界样本数为 $N_g$、多边形顶点数为 $m$, 一次裁剪为 $O(m)$,总计算量主要为 $O(N_qN_g(m+C_{\mathrm{MEC}}(m)))$。 diff --git a/docs/paper/sections/q3.tex b/docs/paper/sections/q3.tex index 0bd75bf..f9739ad 100644 --- a/docs/paper/sections/q3.tex +++ b/docs/paper/sections/q3.tex @@ -52,29 +52,7 @@ $r^2+a^2-\sqrt3ar$。 \begin{figure}[htbp] \centering -\begin{tikzpicture}[x=0.0019cm,y=0.0019cm,font=\small] - \begin{scope} - \clip (0,0) circle (1800); - \fill[inkblue!8] (0,0) circle (1000); - \foreach \angle in {0,60,120,180,240,300}{ - \fill[inkblue!7] (\angle:1159.7) circle (1000); - \draw[inkblue!30] (\angle:1159.7) circle (1000); - } - \draw[inkblue!30] (0,0) circle (1000); - \end{scope} - \draw[thick] (0,0) circle (1800); - \draw[dashed,warmgray] (0,0) circle (1159.7); - \foreach \angle in {0,60,120,180,240,300}{ - \fill[inkblue] (\angle:1159.7) circle (26); - } - \fill[inkblue] (0,0) circle (26) node[below left] {$O$}; - \draw[->] (0,0)--(1159.7,0) node[midway,above] {$a$}; - \draw[->,warmgray] (0,0)--(30:1800) node[midway,above left] {$R$}; - \node[anchor=west,align=left] at (2100,750) - {实线:源位置边界\\虚线:六站所在圆\\浅色圆盘:$1000$ m 接收保证}; - \node[anchor=west,align=left] at (2100,-650) - {$a=1159.7$ m\\$R=1800$ m\\最坏距离 $\le984.539$ m}; -\end{tikzpicture} +\includegraphics[width=0.68\linewidth]{geo-q3-seven-point-coverage.png} \caption{七点全向覆盖的站点构造与接收圆盘。} \label{fig:seven} \end{figure} @@ -178,19 +156,29 @@ C(\pi)=\frac{\norm{p-q_{\pi_1}}}{5} 其收益来自节省额外移动。题设固定同地误差,因此优先选择新的检测位置。 \subsection{算法三与完成条件} -\begin{algorithmblock}{算法3:网格幸存集滤波、信息增益选点与滚动调度(对应 bayes)} -\textbf{输入:}题设参数、七站计划、合法观测接口。\quad -\textbf{输出:}逐频道处理状态与会话统计。 -\begin{enumerate}[leftmargin=2em,itemsep=1pt,topsep=4pt] - \item 检验七点覆盖条件,初始化频道状态、凸候选集、网格幸存集和检测状态矩阵。 - \item 进入后同步记录位置、频道及剩余现实预算,必要时执行有信息的机会测量。 - \item 构建覆盖、定位和清除三类任务;先用网格幸存集跳过必为无信号的检测。 - \item 按最近邻与开放路径 2-opt 排序,执行首任务,用真实响应更新凸候选集与网格幸存集,随后滚动重建任务表。 - \item 若满足清除判据,执行清除;成功则更新状态,失败则追加观测。 - \item 若已清除 $16$ 个源,或每个频道均已清除或有严格无源证据,则退出。 - 否则返回步骤2;预算不足或异常时退出并报告未解决频道。 -\end{enumerate} -\end{algorithmblock} +\begin{algorithm}[htbp] +\caption{网格幸存集滤波、信息增益选点与滚动调度} +\label{alg:q3} +\begin{algorithmic}[1] +\Require 题设参数、七站计划与合法观测接口 +\Ensure 逐频道处理状态与会话统计 +\State 检验七点覆盖条件,初始化频道状态、凸候选集、网格幸存集和检测状态矩阵 +\State 调用 $/enter$,记录位置、频道和剩余现实预算 +\While{存在未处理频道且预算充足} + \State 构建覆盖、定位和清除三类任务 + \State 用网格幸存集跳过必为无信号的检测 + \State 按最近邻与开放路径 2-opt 排序任务表 + \State 执行首任务,根据真实响应更新凸候选集与网格幸存集 + \If{满足清除判据} + \State 执行清除;成功则更新状态,失败则追加观测 + \EndIf + \If{已清除 $16$ 个源或所有频道均被清除或证明无源} + \State \Return 报告完成状态 + \EndIf +\EndWhile +\State 预算不足或异常时退出并报告未解决频道 +\end{algorithmic} +\end{algorithm} “清除数达到 $16$”利用题设数量上界。 对任意已存在的 $16$ 个不同频道,也可提前排除其余未知频道, 但已发现而未清除的源仍必须继续处理。 @@ -200,18 +188,3 @@ C(\pi)=\frac{\norm{p-q_{\pi_1}}}{5} 停滞、协议异常或预算耗尽时,记录各频道状态并终止。 程序以接口返回的现实预算为准,并预留退出时间, 实际计时在整数微秒状态上累计,避免用实机运行秒数代替虚拟动作耗时。 - -\subsection{运行过程示例} -图~\ref{fig:replay3} 回放一局全向演练(案例 \texttt{BBM9-HZG2-77N5-NKKC},$10$ 个源): -左图为移动轨迹、检测示向度与已清除频道,右上方为事件流水, -下方为虚拟时间轴上的检测与清除时刻。 -该局说明覆盖扫描、定位测量与清除三类任务的交替关系, -以及为接收向外发射的源而短时越出 $\Omega$ 边界的路径形态;它属于演练留档,不计入正式成绩。 - -\begin{figure}[htbp] -\centering -\includegraphics[width=0.85\linewidth]{replay-webui-q3-omnidirectional.png} -\caption{全向源演练局(\texttt{BBM9-HZG2-77N5-NKKC})的运行回放。 -该局为演练案例,非正式测试记录。} -\label{fig:replay3} -\end{figure} diff --git a/docs/paper/sections/q4.tex b/docs/paper/sections/q4.tex index 17d720e..5d3b3a3 100644 --- a/docs/paper/sections/q4.tex +++ b/docs/paper/sections/q4.tex @@ -172,36 +172,30 @@ $N_{\mathrm{cell}}\,t_{\mathrm{RTT},95}+t_{\mathrm{reserve}}$。 逐格访问可保证完成该源的清除。 \subsection{算法四与终止条件} -\begin{algorithmblock}{算法4:前向半盘最小命中集、联合信念与后备清除(对应 directional)} -\textbf{输入:}混合源接口,定向覆盖计划与预算。\quad -\textbf{输出:}已清除频道、未解决频道、停止依据。 -\begin{enumerate}[leftmargin=2em,itemsep=1pt,topsep=4pt] - \item 加载覆盖计划;默认 $31$ 点按声明分辨率校核,$49$ 点备选有连续证明。 - \item 选择最近的待测站,当前频道优先,批量检测仍未知的频道。 - \item 同步更新凸位置候选与(位置×朝向)联合幸存集;首次正观测后建立朝向候选。 - \item 处理已发现频道:按可见性概率筛选探点并取移动最省者;满足半径判据则清除,否则继续。 - \item 遮挡或定位停滞时检查格数与预算,允许则执行 $20$ m 方格遍历清除。 - \item 覆盖任务结束后处理未解决频道;主动退出时报告清除数及停止依据。 -\end{enumerate} -\end{algorithmblock} +\begin{algorithm}[htbp] +\caption{前向半盘最小命中集、联合信念与后备清除} +\label{alg:q4} +\begin{algorithmic}[1] +\Require 混合源接口、定向覆盖计划与预算 +\Ensure 已清除频道、未解决频道与停止依据 +\State 加载覆盖计划;默认 $31$ 点按 $15$ m/$72$ 档校核,$49$ 点备选有连续证明 +\While{存在未解决频道且预算充足} + \State 选择最近待测站,当前频道优先,批量检测未知频道 + \State 更新凸位置候选与位置乘朝向联合幸存集;首次正观测后建立朝向候选 + \While{频道尚未清除} + \State 按可见性概率筛选探点,取移动最省者执行测量 + \If{满足半径判据} + \State 执行清除并跳出 + \ElsIf{遮挡或定位停滞} + \State 检查格数与预算,允许则执行 $20$ m 方格后备清除 + \EndIf + \EndWhile +\EndWhile +\State 主动退出时报告清除数与停止依据 +\end{algorithmic} +\end{algorithm} 问题四使用“本站批量扫频、最近候选圆心服务”的调度骨架, 并未直接复用问题三的统一信息增益任务表。 若要求严格的混合源全清证据,除采用连续覆盖计划外, 还须补全类型联合表示或限制联合集排空的适用范围,并确保全部定位清除任务完成。 本文区分当前实现、可证明条件与尚需完善之处,不把后者追溯为正式测试时已执行的功能。 - -\subsection{运行过程示例} -图~\ref{fig:replay4} 回放一局含定向源的演练(案例 \texttt{GHSN-M38E-74GD-YLUD}): -左图在轨迹、检测示向度与已清除频道之外,画出每个定向源的前向半盘 -($\pm90^\circ$,半径取该源的 $r_{\mathrm{eff}}$),箭头方向即朝向。 -图中存在落在接收半径之内、却因位于背向半平面而无信号的检测点, -对应式~\eqref{eq:visibility} 的可见性条件,保留这一解释正是问题四与问题三 -在候选更新上的区别;该局同样不计入正式成绩。 - -\begin{figure}[htbp] -\centering -\includegraphics[width=0.85\linewidth]{replay-webui-q4-directional.png} -\caption{含定向源演练局(\texttt{GHSN-M38E-74GD-YLUD})的运行回放, -紫色半盘为定向源的前向可见区域。该局为演练案例,非正式测试记录。} -\label{fig:replay4} -\end{figure} diff --git a/docs/paper/sections/results.tex b/docs/paper/sections/results.tex index 5b37a7a..33ea100 100644 --- a/docs/paper/sections/results.tex +++ b/docs/paper/sections/results.tex @@ -3,78 +3,52 @@ 本节实验样本严格限定为官方模拟器 \pathtext{Jammers-simulator-full/test} 下的六份正式导出包,问题三、四各有三次。 文件名、案例编码与完整包校验值见结果表与附录~\ref{app:data}。 -本节表格不采用 \texttt{project/results} 中的自建评测数据, -也不将历史演练案例当作正式测试补入:自建配对对比见第~\ref{sec:comparison} 节, -演练局示例见问题三、四的运行过程示例,三类数据分开陈述,不合并统计。 +本节只使用六份正式导出包及其匹配统计,不引入仓库内自建基准集,也不把演练局数据并入正式评价。两类数据仍单独存放;任何跨案例比较都以官方记录的总虚拟时间、程序运行时间和清除数量为准。 -评价指标包括清除数量 $N_c^+$、平均定位清除时间 $\bar T$、程序运行时间 -$T_{\mathrm{real}}$,以及检测、换频和清除的动作次数。 -其中 $\bar T=T/N_c^+$,虚拟时间 $T$ 按整数微秒累计, -$T_{\mathrm{real}}$ 采用官方记录的毫秒值。 -六次测试均以主动退出结束,统计提交状态为已确认。 +评价指标包括清除数量 $N_c^+$、总虚拟时间 $T$、程序运行时间 $T_{\mathrm{real}}$,以及检测、换频和清除的动作次数。其中 $T$ 按整数微秒累计,$T_{\mathrm{real}}$ 采用官方记录的毫秒值;平均定位清除时间仅作辅助分析,不替代总虚拟时间与程序运行时间这两项直接指标。六次测试均以主动退出结束,统计提交状态为已确认。 + +\begin{figure}[htbp] +\centering +\includegraphics[width=0.92\linewidth]{formal-total-time.png} +\caption{六次正式测试的总虚拟时间与程序运行时间。} +\label{fig:formaldirect} +\end{figure} \subsection{问题三正式结果} \begin{table}[htbp] \centering\small -\caption{问题三正式测试结果(时间单位:s,平均时间单位:s/个)} +\caption{问题三正式测试结果(时间单位:s)} \setlength{\tabcolsep}{4pt} \begin{tabularx}{\linewidth}{l X r r r} \toprule -测试 & 测试案例编码 & 清除数 & 平均定位清除时间 & 程序运行时间\\ +测试 & 测试案例编码 & 清除数 & 总虚拟时间/s & 程序运行时间/s\\ \midrule -\csname @@input\endcsname data/formal-q3-rows.tex +测试 1 & \texttt{9HU7-UWJR-BPTR-VYAT} & 13 & 3094.978 & 5.596 \\ +测试 2 & \texttt{7TCG-C9EC-53VF-HZ5H} & 15 & 3493.335 & 3.024 \\ +测试 3 & \texttt{UVUZ-B4MQ-XSUV-HQFP} & 10 & 2906.863 & 2.680 \\ \bottomrule \end{tabularx} \label{tab:formal3} \end{table} -三次正式测试分别清除 $13$、$15$、$10$ 个源,累计 $\QThreeCount$ 个, -平均定位清除时间分别为 $238.075$、$232.889$、$290.686$ s/个。 -三次总虚拟时间之和为 $\QThreeTotal$ s,按清除数量加权的平均时间为 -\begin{equation} -\bar T_{3,\mathrm{w}} -=\frac{\sum_{i=1}^{3}T_{3,i}}{\sum_{i=1}^{3}N_{3,i}^{+}} -=\frac{\QThreeTotal}{\QThreeCount} -=\QThreeWeighted\ \text{s/个}. -\end{equation} -将三个案例等权平均,得到 $\QThreeMean$ s/个。 -加权平均衡量全部已清除源的平均时间成本,等权平均衡量三个案例的平均表现。 - -第三次的总虚拟时间为 $2906.863$ s,在本组三次测试中最短; -其清除数为 $10$,因而每源平均时间为 $290.686$ s/个。 -清除数量与总时间共同决定每源时间,表~\ref{tab:formal3} 同时列出这些指标以便比较。 -程序运行时间为 $2.680$ 至 $5.596$ s,反映了本组案例中的实际计算与通信开销。 +三次正式测试分别清除 $13$、$15$、$10$ 个源,总虚拟时间分别为 $3094.978$、$3493.335$、$2906.863$ s,合计 $\QThreeTotal$ s;程序运行时间分别为 $5.596$、$3.024$、$2.680$ s,合计 $\QThreeProgramTotal$ s。按清除数量加权的平均定位清除时间为 $\QThreeWeighted$ s/个,仅作辅助解释。第三次总虚拟时间 $2906.863$ s 在本组三次中最短,但其清除数 $10$ 也最少;因此判断优化程度时,应以题目要求的总虚拟时间和程序运行时间为直接指标,并同时报告清除数量,不能只看单一的平均值。 \subsection{问题四正式结果} \begin{table}[htbp] \centering\small -\caption{问题四正式测试结果(时间单位:s,平均时间单位:s/个)} +\caption{问题四正式测试结果(时间单位:s)} \setlength{\tabcolsep}{4pt} \begin{tabularx}{\linewidth}{l X r r r} \toprule -测试 & 测试案例编码 & 清除数 & 平均定位清除时间 & 程序运行时间\\ +测试 & 测试案例编码 & 清除数 & 总虚拟时间/s & 程序运行时间/s\\ \midrule -\csname @@input\endcsname data/formal-q4-rows.tex +测试 1 & \texttt{VQUM-ETQ5-74PP-ZQ3J} & 13 & 9362.226 & 7.668 \\ +测试 2 & \texttt{5K5T-TRWZ-BPR2-ECAV} & 10 & 9271.302 & 5.724 \\ +测试 3 & \texttt{K58B-V5VC-E4EF-CRTW} & 14 & 8608.382 & 6.051 \\ \bottomrule \end{tabularx} \label{tab:formal4} \end{table} -表~\ref{tab:formal4} 给出问题四的三次正式结果。 -三次测试分别清除 $13$、$10$、$14$ 个源,累计 $\QFourCount$ 个, -平均定位清除时间分别为 $720.171$、$927.130$、$614.884$ s/个。 -总虚拟时间之和为 $\QFourTotal$ s,相应的加权平均时间为 -\begin{equation} -\bar T_{4,\mathrm{w}} -=\frac{\sum_{i=1}^{3}T_{4,i}}{\sum_{i=1}^{3}N_{4,i}^{+}} -=\frac{\QFourTotal}{\QFourCount} -=\QFourWeighted\ \text{s/个}. -\end{equation} -三个案例的等权平均时间为 $\QFourMean$ s/个。 - -第三次清除数为 $14$,总虚拟时间为 $8608.382$ s, -每源平均时间为 $614.884$ s/个; -在本组三个案例中,其清除数量最多,平均定位清除时间最短。 -程序运行时间为 $5.724$ 至 $7.668$ s。 -各案例的结果变化反映了源分布、有效接收范围和朝向约束对搜索过程的共同影响。 +表~\ref{tab:formal4} 给出问题四的三次正式结果。三次测试分别清除 $13$、$10$、$14$ 个源,总虚拟时间分别为 $9362.226$、$9271.302$、$8608.382$ s,合计 $\QFourTotal$ s;程序运行时间分别为 $7.668$、$5.724$、$6.051$ s,合计 $\QFourProgramTotal$ s。按清除数量加权的平均定位清除时间为 $\QFourWeighted$ s/个,仅作辅助解释。第三次清除数 $14$ 最多且总虚拟时间 $8608.382$ s 最短,说明定向遮挡场景中可见性筛选与后备清除对直接完成时间有积极作用;但三次案例的随机源分布和朝向不同,仍应优先看官方记录的绝对时间和清除数量。 \subsection{动作耗时分解} 将式~\eqref{eq:time} 重排,得到累计移动时间 @@ -95,27 +69,24 @@ T_{\mathrm{move}} \toprule 案例 & 检测数 & 换频数 & 失败清除 & 总虚拟时间/s & 移动时间/s & 移动占比\\ \midrule -\csname @@input\endcsname data/formal-cost-rows.tex +Q3-1 & 105 & 96 & 0 & 3094.978 & 2408.978 & 77.84\% \\ +Q3-2 & 117 & 108 & 0 & 3493.335 & 2725.335 & 78.02\% \\ +Q3-3 & 117 & 107 & 0 & 2906.863 & 2164.863 & 74.47\% \\ +Q4-1 & 349 & 275 & 0 & 9362.226 & 7277.226 & 77.73\% \\ +Q4-2 & 361 & 315 & 0 & 9271.302 & 7101.302 & 76.59\% \\ +Q4-3 & 264 & 213 & 0 & 8608.382 & 7005.382 & 81.38\% \\ \bottomrule \end{tabular} \label{tab:cost} \end{table} -\input{figures/formal-time-bars} - -图~\ref{fig:timingmeasured} 按同一记账口径给出一局全向演练的累计时间构成: -横轴为动作序号,堆叠高度为累计虚拟时间, -移动项在整局中占主要部分,清除阶段表现为阶梯状跃升。 -该图用于说明式~\eqref{eq:time} 的分项在同一局内如何随时间累积, -其数据来自演练留档,不进入正式统计。 - \begin{figure}[htbp] \centering -\includegraphics[width=0.58\linewidth]{timing-measured-q3-10-sources.png} -\caption{一局全向演练(\texttt{BBM9-HZG2-77N5-NKKC},$10$ 个源)的累计虚拟时间构成。 -该局为演练案例,不计入正式测试结果。} -\label{fig:timingmeasured} +\includegraphics[width=0.92\linewidth]{formal-time-bars.png} +\caption{六次正式测试的累计虚拟耗时分解。图中数据仅来自正式测试原始动作计数与总虚拟时间。} +\label{fig:costbars} \end{figure} + 六次测试的移动时间占总虚拟耗时的 $\FormalMovementMin\%$ 至 $\FormalMovementMax\%$, 是主要的时间成本,减少移动因而也是缩短总耗时的主要方向, 但检测与换频仍需一并计入路径选择。 @@ -125,17 +96,7 @@ T_{\mathrm{move}} 该等效距离由官方动作计数与移动速度换算得到,不是独立轨迹测得的长度。 所有平均值先以官方整数单位求和、相除,最后保留三位小数。 -\subsection{实验结果分析} -六次正式测试累计清除 $\FormalClearCount$ 个源, -程序运行时间介于 $\FormalRunMin$ 与 $\FormalRunMax$ s。 -在所观测的三组全向案例中,逐频道网格幸存集融合正负观测, -并与信息增益选点、滚动任务排序相结合(七点解析覆盖承担无源证据), -得到 $232.889$ 至 $290.686$ s/个的平均定位清除时间; -三组混合源案例采用前向半盘31点最小命中集、位置与朝向联合幸存集 -以及预算化后备清除,得到 $614.884$ 至 $927.130$ s/个的相应结果。 +\subsection{直接指标结论} +六次正式测试均主动退出且清除失败计数为零。以题目要求的直接时间指标衡量:问题三三次测试累计清除 $\QThreeCount$ 个源,总虚拟时间 $\QThreeTotal$ s,程序运行时间合计 $\QThreeProgramTotal$ s;问题四累计清除 $\QFourCount$ 个源,总虚拟时间 $\QFourTotal$ s,程序运行时间合计 $\QFourProgramTotal$ s。六次测试合计清除 $\FormalClearCount$ 个源,总虚拟时间 $\FormalVirtualTotal$ s。 -两类问题的共同特征是移动耗时占比较高。 -路径排序应统筹覆盖站、定位测点和清除点的位置, -同时计入频道切换与检测费用。 -平均定位清除时间、累计清除数和程序运行时间分别反映时间成本、 -任务处理规模与计算通信开销,三者共同构成本文的实验评价。 +由此可得结论:1. 全向源场景下,七点解析覆盖与逐频道网格幸存集使三次测试均在正式时限内完成,问题三三次总虚拟时间为 $2906.863$ 至 $3493.335$ s;2. 混合源场景下,前向半盘最小命中集、位置与朝向联合幸存集和后备清除使三次测试总虚拟时间为 $8608.382$ 至 $9362.226$ s,明显高于全向场景,说明定向遮挡仍是时间成本的主要来源;3. 平均定位清除时间和移动时间分解只用于解释成本结构,优化程度的结论应以总虚拟时间、程序运行时间和清除数量为准,不能用自建基准推断倍数。 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 29ca578..030a6f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ requires-python = ">=3.10" dependencies = [ "jupyter>=1.1.1", "requests>=2.31", + "numpy>=1.26", + "matplotlib>=3.8", ] [dependency-groups] @@ -101,3 +103,4 @@ disable = [ "possibly-used-before-assignment", # pytest.importorskip 成功即赋值,skip 会抛异常 ] + diff --git a/scripts/plot_paper_figures.py b/scripts/plot_paper_figures.py new file mode 100644 index 0000000..4dfad36 --- /dev/null +++ b/scripts/plot_paper_figures.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import csv, json, math +from pathlib import Path +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.patches import Arc, Circle, FancyArrowPatch, Polygon, Rectangle, Wedge + +ROOT = Path(__file__).resolve().parents[1] +FIG = ROOT / "docs" / "figures" +FIG.mkdir(parents=True, exist_ok=True) +INK = "#234C70" +GRAY = "#676767" +LIGHT = "#9DB7CA" +PALE = "#D9E4EC" +GOLD = "#B58B3A" +GREEN = "#4E7A55" +plt.rcParams.update({"font.sans-serif": ["DejaVu Sans"], "axes.unicode_minus": False, + "figure.dpi": 120, "savefig.dpi": 300, "axes.edgecolor": GRAY, + "axes.labelcolor": "#222222", "text.color": "#222222"}) + +def save(fig, name): + fig.savefig(FIG / name, bbox_inches="tight", facecolor="white") + plt.close(fig) + +def load_formal(): + rows = [] + path = ROOT / "docs" / "paper" / "data" / "formal-results.csv" + with path.open(encoding="utf-8-sig") as f: + for row in csv.DictReader(f): + for key in ("problem_no", "formal_index", "cleared_jammer_count", + "measure_accepted_count", "channel_switch_count", + "clear_failure_count"): + row[key] = int(row[key]) + for key in ("virtual_time_s", "program_run_duration_s", + "inferred_movement_time_s"): + row[key] = float(row[key]) + rows.append(row) + return rows + +def fig_formal_total(): + rows = load_formal() + labels = ["Q%d-%d" % (r["problem_no"], r["formal_index"]) for r in rows] + total = [r["virtual_time_s"] for r in rows] + runtime = [r["program_run_duration_s"] for r in rows] + colors = [INK, LIGHT, PALE, GOLD, "#C98963", GREEN] + fig, axes = plt.subplots(1, 2, figsize=(10.5, 3.6)) + for ax, vals, title, unit in ((axes[0], total, "Total virtual time", "s"), + (axes[1], runtime, "Program runtime", "s")): + bars = ax.barh(labels[::-1], vals[::-1], color=colors[::-1]) + ax.set_xlabel(title + " / " + unit) + ax.grid(axis="x", color="#E5E5E5", linewidth=0.7) + ax.set_axisbelow(True) + for bar, val in zip(bars, vals[::-1]): + ax.text(bar.get_width() + max(vals) * 0.015, + bar.get_y() + bar.get_height() / 2, + "%.3f" % val, va="center", fontsize=8) + ax.margins(x=0.18) + fig.suptitle("Direct metrics of three formal tests", fontsize=11) + save(fig, "formal-total-time.png") + + +def fig_formal_time_bars(): + rows = load_formal() + labels = ["Q%d-%d" % (r["problem_no"], r["formal_index"]) for r in rows] + move = np.array([r["inferred_movement_time_s"] for r in rows]) + measure = np.array([5.0 * r["measure_accepted_count"] for r in rows]) + switch = np.array([1.0 * r["channel_switch_count"] for r in rows]) + clear = np.array([5.0 * r["cleared_jammer_count"] + 3.0 * r["clear_failure_count"] for r in rows]) + fig, ax = plt.subplots(figsize=(9.0, 3.8)) + ax.barh(labels, move, color=INK, label="Move") + ax.barh(labels, measure, left=move, color=LIGHT, label="Measure") + ax.barh(labels, switch, left=move + measure, color=GRAY, label="Switch") + ax.barh(labels, clear, left=move + measure + switch, color=GOLD, label="Clear") + ax.set_xlabel("Cumulative virtual time / s") + ax.grid(axis="x", color="#E5E5E5", linewidth=0.7) + ax.set_axisbelow(True) + ax.legend(loc="lower right", frameon=False, ncol=4) + save(fig, "formal-time-bars.png") + +def box(ax, x, y, w, h, text, fc=PALE, ec=INK): + ax.add_patch(Rectangle((x, y), w, h, facecolor=fc, edgecolor=ec, linewidth=1.0)) + ax.text(x + w/2, y + h/2, text, ha="center", va="center", fontsize=8, wrap=True) + + +def arrow(ax, p, q, color=GRAY): + ax.add_patch(FancyArrowPatch(p, q, arrowstyle="-|>", mutation_scale=10, + linewidth=1.0, color=color)) + + +def fig_flow_overall(): + fig, ax = plt.subplots(figsize=(8.0, 4.8)) + ax.axis("off") + box(ax, 0.5, 4.0, 7.0, 0.8, "L0 Problem abstraction: area, motion, timing, channels", fc="#EFEFEF") + box(ax, 0.5, 3.0, 2.1, 0.7, "L1 Evidence\ncandidate sets") + box(ax, 2.9, 3.0, 2.1, 0.7, "L1 Decision\ntask tables") + box(ax, 5.3, 3.0, 2.1, 0.7, "L1 Execution\nactions") + box(ax, 0.5, 2.0, 1.6, 0.7, "L2 Q1\ngeometry", fc="#E8EEF4") + box(ax, 2.3, 2.0, 1.6, 0.7, "L2 Q2\nactive measure", fc="#E8EEF4") + box(ax, 4.1, 2.0, 1.6, 0.7, "L2 Q3\nomni loop", fc="#E8EEF4") + box(ax, 5.9, 2.0, 1.6, 0.7, "L2 Q4\nmixed loop", fc="#E8EEF4") + box(ax, 1.4, 0.8, 5.2, 0.7, "L3 Discovery -> Localisation -> Clearance", fc="#E6F0E7", ec=GREEN) + for px in (1.55, 3.95, 6.35): + arrow(ax, (px, 4.0), (px, 3.7)) + for px in (1.3, 3.1, 4.9, 6.7): + arrow(ax, (px, 2.7), (3.9, 1.5)) + ax.set_xlim(0, 8); ax.set_ylim(0, 5.2) + save(fig, "flow-overall-architecture.png") + + +def fig_framework(): + fig, ax = plt.subplots(figsize=(5.8, 4.2)) + ax.axis("off") + box(ax, 0.3, 2.8, 2.0, 0.7, "Observation\nbearing / near / no-signal") + box(ax, 3.1, 2.8, 2.0, 0.7, "Candidate update\ngeometry / grid / heading") + box(ax, 3.1, 1.4, 2.0, 0.7, "Task selection\ncover / locate / clear") + box(ax, 0.3, 1.4, 2.0, 0.7, "Action execution\nmove and timing") + arrow(ax, (2.3, 3.15), (3.1, 3.15)); arrow(ax, (4.1, 2.8), (4.1, 2.1)) + arrow(ax, (3.1, 1.75), (2.3, 1.75)); arrow(ax, (1.3, 2.1), (1.3, 2.8)) + box(ax, 3.1, 0.3, 2.0, 0.6, "Exit / report", fc="#EFEFEF") + arrow(ax, (4.1, 1.4), (4.1, 0.9)) + ax.set_xlim(0, 5.6); ax.set_ylim(0, 4.0) + save(fig, "framework-decision-loop.png") + + +def fig_state_machine(): + fig, ax = plt.subplots(figsize=(7.0, 2.2)) + ax.axis("off") + xs = [(0.1, "IDLE"), (1.8, "ENTER\n5 s countdown"), (3.8, "OPEN\ninterface"), + (5.8, "CLOSED\nexit / timeout")] + for x, t in xs: + box(ax, x, 0.6, 1.5, 0.8, t) + for x in (1.6, 3.6, 5.6): + arrow(ax, (x, 1.0), (x + 0.2, 1.0)) + ax.set_xlim(0, 7.4); ax.set_ylim(0, 2.0) + save(fig, "steps-state-machine.png") + + +def fig_timing_attachment(): + steps = ["step 1", "step 2", "step 3", "step 4"] + move = np.array([100.0, 0.0, 80.0, 0.0]) + action = np.array([5.0, 5.0, 3.0, 5.0]) + switch = np.array([0.0, 1.0, 0.0, 0.0]) + fig, ax = plt.subplots(figsize=(6.0, 3.2)) + ax.bar(steps, move, color=INK, label="Move") + ax.bar(steps, action, bottom=move, color=LIGHT, label="Measure / clear") + ax.bar(steps, switch, bottom=move + action, color=GRAY, label="Switch") + for i, total in enumerate(move + action + switch): + ax.text(i, total + 2, "%.0f" % total, ha="center", fontsize=8) + ax.set_ylabel("Virtual time / s") + ax.legend(frameon=False) + save(fig, "timing-attachment-example-105-111-194-199.png") + +def clip_halfplane(poly, a, b, c): + if len(poly) == 0: + return poly + out = [] + for i in range(len(poly)): + p = poly[i]; q = poly[(i + 1) % len(poly)] + fp = a * p[0] + b * p[1] - c + fq = a * q[0] + b * q[1] - c + if fp <= 1e-9: + out.append(p) + if (fp < -1e-9 and fq > 1e-9) or (fp > 1e-9 and fq < -1e-9): + t = fp / (fp - fq) + out.append(p + t * (q - p)) + return np.array(out, dtype=float) + + +def wedge_halfplanes(sx, sy, theta_deg, delta_deg): + theta = math.radians(theta_deg); delta = math.radians(delta_deg) + a1 = math.sin(theta - delta); b1 = -math.cos(theta - delta) + c1 = a1 * sx + b1 * sy + a2 = -math.sin(theta + delta); b2 = math.cos(theta + delta) + c2 = a2 * sx + b2 * sy + a3 = -math.cos(theta); b3 = -math.sin(theta) + c3 = a3 * sx + b3 * sy + return [(a1, b1, c1), (a2, b2, c2), (a3, b3, c3)] + + +def intersection_polygon(meas, bound=2500.0): + poly = np.array([[-bound, -bound], [bound, -bound], [bound, bound], [-bound, bound]], dtype=float) + for m in meas: + for a, b, c in wedge_halfplanes(m["x"], m["y"], m["bearing_deg"], m["half_angle_deg"]): + poly = clip_halfplane(poly, a, b, c) + if len(poly) == 0: + return poly + return poly + + +def pair_circle(p, q): + c = (p + q) / 2.0 + r = np.linalg.norm(p - q) / 2.0 + return c, r + + +def contains_all(c, r, pts, tol=1e-6): + return all(np.linalg.norm(p - c) <= r + tol for p in pts) + + +def circumcircle(p, q, r): + ax, ay = p; bx, by = q; cx, cy = r + d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) + if abs(d) < 1e-12: + return None + ux = ((ax*ax + ay*ay) * (by - cy) + (bx*bx + by*by) * (cy - ay) + (cx*cx + cy*cy) * (ay - by)) / d + uy = ((ax*ax + ay*ay) * (cx - bx) + (bx*bx + by*by) * (ax - cx) + (cx*cx + cy*cy) * (bx - ax)) / d + c = np.array([ux, uy]) + return c, np.linalg.norm(c - p) + + +def min_enclosing_circle(pts): + pts = np.asarray(pts, dtype=float) + best_c = pts[0]; best_r = 0.0 + for i in range(len(pts)): + for j in range(i + 1, len(pts)): + c, r = pair_circle(pts[i], pts[j]) + if contains_all(c, r, pts) and r < best_r or (best_r == 0.0 and contains_all(c, r, pts)): + best_c, best_r = c, r + for i in range(len(pts)): + for j in range(i + 1, len(pts)): + for k in range(j + 1, len(pts)): + cc = circumcircle(pts[i], pts[j], pts[k]) + if cc is None: + continue + c, r = cc + if contains_all(c, r, pts) and r < best_r: + best_c, best_r = c, r + return best_c, best_r + + +def fig_q1_counterexample(): + A = np.array([0.0, 0.0]); B = np.array([40.0, 0.0]); C = np.array([20.0, 20.0 * math.sqrt(3)]) + fig, ax = plt.subplots(figsize=(4.6, 4.2)) + ax.add_patch(Polygon([A, B, C], closed=True, fill=False, edgecolor=INK, linewidth=1.4)) + dc, dr = pair_circle(A, B); ax.add_patch(Circle(dc, dr, fill=False, edgecolor=GOLD, linestyle="--", linewidth=1.2)) + mc, mr = min_enclosing_circle(np.array([A, B, C])); ax.add_patch(Circle(mc, mr, fill=False, edgecolor=GREEN, linewidth=1.4)) + ax.scatter([A[0], B[0], C[0]], [A[1], B[1], C[1]], color=INK, s=12) + ax.text(C[0] + 0.8, C[1] + 0.6, "C", fontsize=9) + ax.text(-1.2, -2.0, "A", fontsize=9); ax.text(40.0, -2.0, "B", fontsize=9) + ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE"); ax.set_xlim(-8, 52); ax.set_ylim(-8, 42) + ax.set_xlabel("x / m"); ax.set_ylabel("y / m") + ax.legend(["triangle", "diameter circle (r=20)", "minimum enclosing circle"], frameon=False, fontsize=7, loc="upper left") + save(fig, "geo-q1-diameter-circle-counterexample.png") + + +def fig_q1_meetup(): + data_path = ROOT / "examples" / "q1" / "triangle.json" + meas = json.loads(data_path.read_text(encoding="utf-8"))["measurements"] + poly = intersection_polygon(meas, bound=2500.0) + if len(poly) < 3: + poly = np.array([[0.0, 0.0], [40.0, 0.0], [40.0, 30.0], [0.0, 30.0]]) + fig, axes = plt.subplots(1, 2, figsize=(8.2, 4.0)) + ax = axes[0] + for m in meas: + x, y = m["x"], m["y"]; th = math.radians(m["bearing_deg"]); d = math.radians(m["half_angle_deg"]) + for sgn in (-1.0, 1.0): + ang = th + sgn * d + ray = np.array([x, y]) + 3600.0 * np.array([math.cos(ang), math.sin(ang)]) + ax.plot([x, ray[0]], [y, ray[1]], color=LIGHT, linewidth=0.8) + ax.scatter([x], [y], color=GRAY, s=14) + ax.text(x + 40, y + 40, "S", fontsize=8) + ax.add_patch(Polygon(poly, closed=True, facecolor=PALE, edgecolor=INK, linewidth=1.4)) + ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE") + ax.set_xlim(-1200, 1000); ax.set_ylim(-1000, 1100) + ax.set_xlabel("x / m"); ax.set_ylabel("y / m") + ax.set_title("Wedge intersection", fontsize=9) + ax2 = axes[1] + ax2.add_patch(Polygon(poly, closed=True, facecolor=PALE, edgecolor=INK, linewidth=1.4)) + c, r = min_enclosing_circle(poly) + d = max(np.linalg.norm(poly[i] - poly[j]) for i in range(len(poly)) for j in range(i + 1, len(poly))) + dc, dr = pair_circle(poly[0], poly[np.argmax(np.linalg.norm(poly - poly[0], axis=1))]) + ax2.add_patch(Circle(dc, dr, fill=False, edgecolor=GOLD, linestyle="--", linewidth=1.1)) + ax2.add_patch(Circle(c, r, fill=False, edgecolor=GREEN, linewidth=1.3)) + ax2.scatter(poly[:, 0], poly[:, 1], color=INK, s=10) + lo = poly.min(axis=0) - 4; hi = poly.max(axis=0) + 4 + ax2.set_xlim(lo[0], hi[0]); ax2.set_ylim(lo[1], hi[1]) + ax2.set_aspect("equal"); ax2.grid(True, color="#EEEEEE") + ax2.set_title("Zoom: diameter vs. enclosing circle", fontsize=9) + ax2.set_xlabel("x / m"); ax2.set_ylabel("y / m") + save(fig, "geo-q1-convex-intersection-zoom.png") + +def fig_q2_safe_disk(): + delta = math.radians(1.0); rp = 1500.0; a = rp / (2.0 * math.cos(delta)); safe = 1000.0 - a + fig, axes = plt.subplots(1, 2, figsize=(8.0, 3.6)) + ax = axes[0] + ax.plot([0, rp * math.cos(delta)], [0, rp * math.sin(delta)], color=GRAY, linewidth=0.8) + ax.plot([0, rp * math.cos(delta)], [0, -rp * math.sin(delta)], color=GRAY, linewidth=0.8) + ax.add_patch(Circle((a, 0), a, fill=False, edgecolor=LIGHT, linestyle="--", linewidth=1.2)) + ax.add_patch(Circle((a, 0), safe, facecolor=PALE, edgecolor=INK, linewidth=1.2, alpha=0.8)) + ax.scatter([0], [0], color=GRAY, s=12); ax.scatter([a], [0], color=INK, s=12) + ax.text(a + 20, 20, "c", fontsize=9); ax.text(10, 15, "s", fontsize=9) + ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE") + ax.set_xlim(-80, rp + 80); ax.set_ylim(-320, 320) + ax.set_xlabel("x / m"); ax.set_ylabel("y / m"); ax.set_title("Truncated wedge and safe disk", fontsize=9) + ax = axes[1] + ax.plot([0, 0], [0, 1], color=GRAY, linewidth=2) + ax.plot([0, 1], [0, 0], color=INK, linewidth=4) + ax.text(0.5, -0.08, "a = 1500/(2cos delta)", ha="center", fontsize=8) + ax.text(0.5, 0.55, "safe radius = 1000 - a", ha="center", fontsize=8) + ax.axis("off") + save(fig, "geo-q2-guaranteed-reception-disk.png") + + +def fig_q3_coverage(): + a = 1159.7; R = 1800.0 + fig, ax = plt.subplots(figsize=(5.2, 5.0)) + ax.add_patch(Circle((0, 0), R, fill=False, edgecolor=INK, linewidth=1.4)) + stations = [(0.0, 0.0)] + [(a * math.cos(k * math.pi / 3.0), a * math.sin(k * math.pi / 3.0)) for k in range(6)] + for x, y in stations: + ax.add_patch(Circle((x, y), 1000.0, facecolor=PALE, edgecolor=LIGHT, linewidth=0.7, alpha=0.35)) + ax.scatter([x], [y], color=INK, s=16) + ax.add_patch(Circle((0, 0), a, fill=False, edgecolor=GRAY, linestyle="--", linewidth=1.0)) + ax.text(0, 0, "O", fontsize=9, ha="right", va="top"); ax.text(a, 0, "a", fontsize=9, ha="center", va="bottom") + ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE"); ax.set_xlim(-2000, 2000); ax.set_ylim(-2000, 2000) + ax.set_xlabel("x / m"); ax.set_ylabel("y / m"); ax.set_title("Seven-point coverage", fontsize=9) + save(fig, "geo-q3-seven-point-coverage.png") + + +def fig_q4_plan(): + data = json.loads((ROOT / "src" / "mcm26b" / "guarantee" / "plans" / "q4_mincover_v2.json").read_text(encoding="utf-8")) + pts = np.array(data["points"], dtype=float) + fig, ax = plt.subplots(figsize=(5.4, 5.0)) + ax.add_patch(Circle((0, 0), 1800.0, facecolor=PALE, edgecolor=INK, linewidth=1.1, alpha=0.45)) + ax.scatter(pts[:, 0], pts[:, 1], color=INK, s=18, label="31 stations") + ax.scatter([0], [0], color=GOLD, marker="*", s=50, label="origin") + ax.arrow(1200, 1200, 450, 0, head_width=70, head_length=100, color=GREEN, length_includes_head=True) + ax.add_patch(Wedge((1200, 1200), 650, -90, 90, facecolor=GREEN, alpha=0.10, edgecolor=GREEN, linewidth=0.8)) + ax.text(1250, 1250, "forward half-disk", fontsize=8) + ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE"); ax.set_xlim(-2800, 2800); ax.set_ylim(-2800, 2800) + ax.set_xlabel("x / m"); ax.set_ylabel("y / m"); ax.legend(frameon=False, fontsize=8, loc="lower right") + save(fig, "geo-q4-minimum-cover-31-points.png") + + +def main(): + fig_formal_total() + fig_formal_time_bars() + fig_flow_overall() + fig_framework() + fig_state_machine() + fig_timing_attachment() + fig_q1_counterexample() + fig_q1_meetup() + fig_q2_safe_disk() + fig_q3_coverage() + fig_q4_plan() + + +if __name__ == "__main__": + main() + +