论文完成

This commit is contained in:
2026-09-13 18:05:59 +08:00
parent 9fb15f53f5
commit 89021eb9f8
234 changed files with 28597 additions and 2164 deletions
+176 -50
View File
@@ -9,6 +9,7 @@ is exempt from the line-length check (the only pylint exception in scripts/).
from __future__ import annotations
import argparse
import csv
import hashlib
import json
import zipfile
@@ -24,6 +25,16 @@ def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def write_tex(path: Path, text: str) -> None:
"""写 LaTeX 片段;强制 LF 换行。
Windows 下的 CRLF 会让 booktabs 的 ``\\bottomrule`` 在表格内部变成
"Misplaced \\noalign":``\\input`` 读入的首行在 TeX 缓冲中已被 ``\\par`` 化,
表格因此提前结束。所有 ``data/*.tex`` 一律使用 LF。
"""
path.write_text(text.replace("\r\n", "\n").replace("\r", "\n"), "utf-8", newline="\n")
def figures(rows: list[dict]) -> None:
out = PAPER / "figures"
out.mkdir(parents=True, exist_ok=True)
@@ -48,7 +59,7 @@ def figures(rows: list[dict]) -> None:
r"\caption{第四问集合覆盖模型得到的31个检测站及源位置区域。}",
r"\label{fig:q4plan}\end{figure}",
]
(out / "q4-plan.tex").write_text("\n".join(lines) + "\n", "utf-8")
write_tex(out / "q4-plan.tex", "\n".join(lines) + "\n")
colors = ["inkblue", "inkblue!50", "warmgray!70", "black!18"]
bars = [
r"\begin{figure}[htbp]\centering",
@@ -79,7 +90,7 @@ def figures(rows: list[dict]) -> None:
r"\caption{正式测试的累计虚拟耗时分解。移动时间由总耗时与各动作计数计算。}",
r"\label{fig:costbars}\end{figure}",
]
(out / "formal-time-bars.tex").write_text("\n".join(bars) + "\n", "utf-8")
write_tex(out / "formal-time-bars.tex", "\n".join(bars) + "\n")
def fixed(value: float | None, digits: int = 3) -> str:
@@ -171,33 +182,10 @@ def project_tables() -> None:
("ProjQFourMoveBase", fixed(four["baseline"]["move_distance_mean_m"] / 1000)),
]
lines = [head] + [f"\\newcommand{{\\{name}}}{{{value}}}" for name, value in macros]
(out / "project-macros.tex").write_text("\n".join(lines) + "\n", "utf-8")
write_tex(out / "project-macros.tex", "\n".join(lines) + "\n")
def write(stem: str, rows: list[str]) -> None:
(out / stem).write_text("\n".join([head, *rows]) + "\n", "utf-8")
write("project-overview-rows.tex", [
"问题 & 核心指标 & 最优实现 & 基线实现 " + r"\\",
f"1 & 直径平均绝对误差 / 覆盖判定准确率 & {fixed(random['diameter_mae_optimal_m'])} m / "
f"{percent(random['coverage_accuracy_optimal'])} & {fixed(random['diameter_mae_baseline_m'])} m / "
f"{percent(random['coverage_accuracy_baseline'])} " + r"\\",
f"2 & 保证接收率 / 最坏后验半径均值 & {percent(two['optimal']['guaranteed_rate'])} / "
f"{fixed(two['optimal']['worst_posterior_mean_m'])} m & {percent(two['baseline_mec']['guaranteed_rate'])} / "
f"{fixed(two['baseline_mec']['worst_posterior_mean_m'])} m " + r"\\",
f"3 & 全清率 / 平均虚拟耗时 & {percent(three['optimal']['cleared_ratio_mean'])} / "
f"{fixed(three['optimal']['virtual_time_mean_s'])} s & {percent(three['baseline']['cleared_ratio_mean'])} / "
f"{fixed(three['baseline']['virtual_time_mean_s'])} s " + r"\\",
f"4 & 全清率 / 缺席误判总数 & {percent(four['optimal']['cleared_ratio_mean'])} / "
f"{four['optimal']['absent_misjudged_total']} & {percent(four['baseline']['cleared_ratio_mean'])} / "
f"{four['baseline']['absent_misjudged_total']} " + r"\\",
])
write("project-q1-example-rows.tex", [
f"{row['name']} & {fixed(row['diameter_ref_m'])} & {fixed(row['diameter_opt_m'])} & "
f"{fixed(row['diameter_base_m'])} & {fixed(row['abs_err_base_m'])} & "
f"{flag(row['covers_ref'])} & {flag(row['covers_opt'])} & {flag(row['covers_base'])} " + r"\\"
for row in data["q1"]["examples"]
])
write_tex(out / stem, "\n".join([head, *rows]) + "\n")
write("project-q1-summary-rows.tex", [
f"最优(固定 {examples['cases']} 例) & {percent(examples['status_accuracy_optimal'])} & "
@@ -214,14 +202,6 @@ def project_tables() -> None:
f"{percent(random['coverage_accuracy_baseline'])} & {fixed(random['runtime_baseline_mean_ms'])} " + r"\\",
])
write("project-q2-rows.tex", [
f"{label} & {percent(two[key]['guaranteed_rate'])} & {fixed(two[key]['worst_posterior_mean_m'])} & "
f"{fixed(two[key]['worst_posterior_median_m'])} & {fixed(two[key]['improvement_mean_m'])} & "
f"{fixed(two[key]['final_ratio_mean'])} & {fixed(two[key]['move_cost_mean_s'], 2)} " + r"\\"
for key, label in (("optimal", "算法2(默认选点)"), ("reference", "参考:盘内网格枚举"),
("baseline_perp", "基线:垂直 1000 m"), ("baseline_mec", "基线:候选圆心"))
])
def variant_rows(table: dict, mapping: list[tuple[str, str]]) -> list[str]:
return [
f"{label} & {table[key]['valid_episodes']}/{table[key]['episodes']} & "
@@ -233,25 +213,65 @@ def project_tables() -> None:
for key, label in mapping
]
write("project-q3-rows.tex", variant_rows(three, [
("optimal", "bayes(默认策略)"), ("seven", "seven(七点批量)"), ("baseline", "grid(49 点网格)")]))
write("project-q4-rows.tex", variant_rows(four, [
("optimal", "directional(默认策略)"), ("baseline_grid", "grid(问题三网格)"),
("baseline", "bayes(问题三策略)")]))
def table_environment(caption: str, label: str, colspec: str, header: str,
body: list[str], placement: str = "htbp",
tabcolsep: str = "4pt") -> str:
"""整表输出:表格不能被 ``\\input`` 切开,否则注释行会在表内注入 ``\\par``。"""
return "\n".join([
f"\\begin{{table}}[{placement}]",
"\\centering\\small",
f"\\caption{{{caption}}}",
f"\\setlength{{\\tabcolsep}}{{{tabcolsep}}}",
f"\\begin{{tabular}}{{{colspec}}}",
"\\toprule",
header,
"\\midrule",
*body,
"\\bottomrule",
"\\end{tabular}",
f"\\label{{{label}}}",
"\\end{table}",
"",
])
write("project-q3-table.tex", [table_environment(
f"问题三:三种策略在 {three['optimal']['episodes']} 个全向源场景上的对比(时间:s;"
f"三种策略的缺席误判均为 {three['optimal']['absent_misjudged_total']})",
"tab:q3compare", "lrrrrrrrr",
"策略 & 有效局 & 全清率 & $T$ 均值 & $T$ 中位 & $T$ 最大 & 检测均值 & "
"位移/km & 后备清除" + r"\\",
# 问题三三种策略的缺席误判均为 0,该列改写入表题以控制表宽。
[" & ".join(c for i, c in enumerate(row.split(" & ")) if i != 3) + r" \\"
for row in variant_rows(three, [
("optimal", "bayes(默认策略)"), ("seven", "seven(七点批量)"),
("baseline", "grid(49 点网格)")])], tabcolsep="2pt")])
write("project-q4-table.tex", [table_environment(
f"问题四:三种策略在 {four['optimal']['episodes']} 个混合源场景上的对比(时间:s)",
"tab:q4compare", "lrrrrrrr",
"策略 & 有效局 & 全清率 & 缺席误判 & $T$ 均值 & $T$ 中位 & $T$ 最大 & 检测均值" + r"\\",
# 问题四表去掉位移与后备清除两列(正文单独说明后备清除次数),以控制表宽。
[" & ".join(row.split(" & ")[:8]) + r" \\"
for row in variant_rows(four, [
("optimal", "directional(默认策略)"), ("baseline_grid", "grid(问题三网格)"),
("baseline", "bayes(问题三策略)")])], tabcolsep="4pt")])
def listings(rows: list[dict]) -> None:
"""生成正式数据清单;核心函数源码清单由 build_code_appendix.py 单独生成。
"""生成附录的正式数据表格;核心函数源码清单由 build_code_appendix.py 单独生成。
论文附录只收录四问主线的核心函数(数据源为单文件核心代码 ipynb),
评测机、命令行外壳、采集/绘图脚本、测试与未采用方案不再逐字排版。
正式结果表同样整表输出:被 ``\\input`` 的文件若以注释行开头,
会在表格内部注入 ``\\par``,使 booktabs 的 ``\\bottomrule`` 报错。
"""
data = PAPER / "data"
head = ("% Generated by scripts/build_paper_support.py; 本文件是完整 table 环境,"
"供附录直接 \\input。")
table = [
r"\begin{longtable}{p{1.15cm}p{13.3cm}}",
r"\caption{所选正式导出文件与完整包哈希}\label{tab:packages}\\",
r"\toprule 案例 & 原始文件名及 SHA-256\\\midrule\endfirsthead",
r"\toprule 案例 & 原始文件名及 SHA-256\\\midrule\endhead",
r"\toprule 案例 & 原始文件名与 SHA-256\\\midrule\endhead",
]
for row in rows:
h = row["package_sha256"]
@@ -261,13 +281,69 @@ def listings(rows: list[dict]) -> None:
fr"& {{\scriptsize\texttt{{{h[:32]}}}\allowbreak\texttt{{{h[32:]}}}}}\\[4pt]",
]
table += [r"\bottomrule\end{longtable}"]
(data / "formal-file-list.tex").write_text("\n".join(table) + "\n", "utf-8")
write_tex(data / "formal-file-list.tex", "\n".join(table) + "\n")
def formal_table(caption: str, label: str, header: str, body: list[str]) -> str:
return "\n".join([
r"\begin{table}[htbp]",
r"\centering\small",
f"\\caption{{{caption}}}",
r"\setlength{\tabcolsep}{4pt}",
r"\begin{tabularx}{\linewidth}{l X r r r}",
r"\toprule",
header,
r"\midrule",
*body,
r"\bottomrule",
r"\end{tabularx}",
f"\\label{{{label}}}",
r"\end{table}",
"",
])
formal_head = r"测试 & 测试案例编码 & 清除数 & 总虚拟时间/s & 程序运行时间/s\\"
for problem in (3, 4):
subset = sorted((r for r in rows if r["problem_no"] == problem),
key=lambda r: r["formal_index"])
body = [
f"测试 {r['formal_index']} & \\texttt{{{r['case_code']}}} & "
f"{r['cleared_jammer_count']} & {fixed(r['virtual_time_s'])} & "
f"{fixed(r['program_run_duration_s'])} " + r"\\"
for r in subset
]
write_tex(data / f"formal-q{problem}-table.tex", "\n".join([head, formal_table(
f"问题{'三四'[problem - 3]}正式测试结果(时间单位:s)",
f"tab:formal{problem}", formal_head, body)]))
body = []
for r in sorted(rows, key=lambda r: (r["problem_no"], r["formal_index"])):
body.append(
f"Q{r['problem_no']}-{r['formal_index']} & {r['measure_accepted_count']} & "
f"{r['channel_switch_count']} & {r['clear_failure_count']} & "
f"{fixed(r['virtual_time_s'])} & {fixed(r['inferred_movement_time_s'])} & "
f"{percent(r['movement_share_percent'] / 100)} " + r"\\")
write_tex(data / "formal-cost-table.tex", "\n".join([
head,
r"\begin{table}[htbp]",
r"\centering\small",
r"\caption{六次正式测试的动作计数与移动耗时}",
r"\setlength{\tabcolsep}{4.5pt}",
r"\begin{tabular}{lrrrrrr}",
r"\toprule",
r"案例 & 检测数 & 换频数 & 失败清除 & 总虚拟时间/s & 移动时间/s & 移动占比\\",
r"\midrule",
*body,
r"\bottomrule",
r"\end{tabular}",
r"\label{tab:formalcost}",
r"\end{table}",
""]))
SUPPORT_README = """# 论文支撑材料
汇总官方模拟器 test 中的六份正式导出及哈希匹配的官方统计。
问题三、四各包含三次正式测试。支撑材料保留原始文件名及文件校验值。
汇总自建模拟器(评测机 `referee/`)的演练局日志与官方模拟器 `test` 中的六份正式导出。
正文的机制论证与定量结论来自演练局,官方正式数据只用于附录中的效果验证。
## 环境和运行
@@ -280,15 +356,19 @@ Python >=3.10;安装 requests,可选 numpy 加速。
python -m mcm26b.cli run --problem 4 --strategy directional --robot-id <模拟器登录队号>
在线命令需要先由使用者在官方模拟器中选择测试、待接口就绪。
策略的自检命令另需可选 referee 包。
自建模拟器可用 `python -m referee` 启动,演练局日志写入 `referee-data/sessions/`。
## 重算论文数据和图表
python scripts/build_practice_data.py # 演练局复算,生成正文数据与宏文件
python scripts/plot_paper_figures.py # 生成正文全部插图
python scripts/audit_paper_formal.py --snapshot docs/paper/data/formal-results.json
python scripts/build_paper_support.py
python scripts/build_paper_support.py # 附录正式数据表与支撑包
上述命令在本地重算统计表、图表及源码附录。统计快照来自官方正式
统计队列,通过票据哈希匹配所选包。重新验证来源匹配需原电脑的统计队列。
第一条命令按题面计时模型复算 `referee-data/sessions/` 下的每局演练日志,
输出论文引用的宏、表格与插图数据;复算值与日志记录的最大偏差为 0.000000 s。
正式统计快照来自官方统计队列,通过票据哈希匹配所选包;
重新验证来源匹配需原电脑的统计队列。
连续覆盖与混合类型推断的扩展见论文未来工作。
## 编译
@@ -299,6 +379,8 @@ Python >=3.10;安装 requests,可选 numpy 加速。
版式使用原项目模板。编译前请将原项目 docs/paper/fonts 目录放回
同一相对位置。使用 latexmk 自动完成交叉引用所需的多遍编译。
`data/*.tex` 由脚本生成,一律使用 LF 换行:CRLF 会让 `\\input` 的表格行
在 TeX 缓冲中提前 `\\par` 化,触发 booktabs 的 "Misplaced \\noalign"。
support-manifest.json 列出包内全部材料(清单自身除外)及 SHA-256。
"""
@@ -314,11 +396,23 @@ def package(rows: list[dict]) -> Path:
selected.add(ROOT / "docs/figures/README.md")
selected.update(path for path in (ROOT / "project").rglob("*")
if path.is_file() and "__pycache__" not in path.parts)
# 自建模拟器与其行为日志是正文数据的来源,必须随支撑包提交。
selected.update(path for path in (ROOT / "referee").rglob("*")
if path.is_file() and "__pycache__" not in path.parts
and ".pytest_cache" not in path.parts)
selected.update((ROOT / "referee-data/sessions").glob("*/*.json*"))
# 被排除的演练局也随包提交:论文附录用它们说明"未优化完全"的判定依据。
selected.update((ROOT / "referee-data/rejected-sessions").glob("*/*.json*"))
selected.update((ROOT / "referee-data/superseded-sessions").glob("*/*.json*"))
selected.update((ROOT / "examples/replay").glob("*.jsonl"))
selected.update([PAPER / "main.tex", PAPER / "mcm26b-paper.cls",
PAPER / "README.md", PAPER / "REWRITE_NOTES.md",
PAPER / "data/formal-macros.tex",
ROOT / "mcm26b-核心代码.ipynb",
ROOT / "scripts/audit_paper_formal.py",
ROOT / "scripts/build_code_appendix.py",
ROOT / "scripts/build_practice_data.py",
ROOT / "scripts/run_practice_episodes.py",
ROOT / "scripts/build_paper_support.py"])
for row in rows:
path = ROOT / "Jammers-simulator-full" / row["file"]
@@ -342,11 +436,43 @@ def package(rows: list[dict]) -> Path:
return out
def formal_rows() -> list[dict]:
"""读正式测试快照。
表格需要逐局的移动时间与位移占比,这两项只在 ``formal-results.csv`` 中,
哈希与文件名清单所需的 ``package_sha256`` 则来自 JSON 快照,故两者合并。
"""
records = json.loads((PAPER / "data/formal-results.json").read_text("utf-8"))["records"]
numbers = {r["case_code"]: r for r in csv_rows()}
merged = []
for record in records:
extra = numbers.get(record["case_code"], {})
merged.append({**record, **{k: v for k, v in extra.items() if k not in record}})
return merged
def csv_rows() -> list[dict]:
"""按 CSV 表头类型读回 ``formal-results.csv``。"""
numeric_int = ("problem_no", "formal_index", "cleared_jammer_count",
"measure_accepted_count", "channel_switch_count", "clear_failure_count")
numeric_float = ("virtual_time_s", "program_run_duration_s",
"inferred_movement_time_s", "movement_share_percent")
rows = []
with (PAPER / "data/formal-results.csv").open(encoding="utf-8-sig") as handle:
for row in csv.DictReader(handle):
for key in numeric_int:
row[key] = int(row[key])
for key in numeric_float:
row[key] = float(row[key])
rows.append(row)
return rows
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--package", action="store_true")
args = parser.parse_args()
rows = json.loads((PAPER / "data/formal-results.json").read_text("utf-8"))["records"]
rows = formal_rows()
figures(rows)
project_tables()
listings(rows)