"""Build vector figure sources, complete source listings, and a compact support ZIP. Formal figures use the audited snapshot only. Geometric figures use declared algorithm parameters, not fabricated observations or hidden simulator data. """ from __future__ import annotations import argparse import hashlib import json import zipfile from decimal import Decimal from pathlib import Path ROOT = Path(__file__).resolve().parents[1] PAPER = ROOT / "docs/paper" # Display copies only: executable Python files remain byte-for-byte unchanged. ESCAPE_OPEN = "(" + "*@" ESCAPE_CLOSE = "@*" + ")" LATEX_SYMBOLS = { "\u03b1": r"\alpha", "\u03b4": r"\delta", "\u03b8": r"\theta", "\u03bb": r"\lambda", "\u03c0": r"\pi", "\u03c1": r"\rho", "\u03c6": r"\varphi", "\u2308": r"\lceil", "\u2309": r"\rceil", "\u2200": r"\forall", "\u2203": r"\exists", "\u2205": r"\varnothing", "\u2208": r"\in", "\u2227": r"\wedge", "\u2229": r"\cap", "\u222a": r"\cup", "\u2248": r"\approx", "\u2264": r"\le", "\u2265": r"\ge", "\u2286": r"\subseteq", "\u2287": r"\supseteq", "\u27f9": r"\Longrightarrow", } def escaped_listing(source: str) -> str: if ESCAPE_OPEN in source or ESCAPE_CLOSE in source: raise ValueError("Source collides with the LaTeX escape delimiters") return "".join( ESCAPE_OPEN + r"\ensuremath{" + LATEX_SYMBOLS[c] + "}" + ESCAPE_CLOSE if c in LATEX_SYMBOLS else c for c in source ) def digest(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def figures(rows: list[dict]) -> None: out = PAPER / "figures" out.mkdir(parents=True, exist_ok=True) plan = json.loads((ROOT / "src/mcm26b/guarantee/plans/q4_mincover_v2.json").read_text("utf-8")) lines = [ r"\begin{figure}[htbp]\centering", r"\begin{tikzpicture}[x=0.00145cm,y=0.00145cm,font=\small]", r"\fill[inkblue!5] (0,0) circle (1800);", r"\draw[thick] (0,0) circle (1800);", r"\draw[->,warmgray] (-2900,0)--(2950,0) node[right] {$x$/m};", r"\draw[->,warmgray] (0,-2800)--(0,2850) node[above] {$y$/m};", ] for x in (-2000, -1000, 1000, 2000): lines += [fr"\draw ({x},-35)--({x},35);", fr"\node[below,font=\scriptsize] at ({x},-50) {{{x}}};"] for x, y in plan["points"]: lines.append(fr"\fill[inkblue] ({x},{y}) circle (33);") lines += [ r"\node[anchor=west,align=left] at (3250,900) {圆域:源位置先验\\蓝点:31个检测站};", r"\node[anchor=west,align=left] at (3250,-700) {部分站点在圆域外\\用于接收向外发射的源};", r"\end{tikzpicture}", r"\caption{第四问31点计划的几何分布。站点取自计划文件,不代表正式运行轨迹;离散验证不等同于连续覆盖证明。}", r"\label{fig:q4plan}\end{figure}", ] (out / "q4-plan.tex").write_text("\n".join(lines) + "\n", "utf-8") colors = ["inkblue", "inkblue!50", "warmgray!70", "black!18"] bars = [ r"\begin{figure}[htbp]\centering", r"\begin{tikzpicture}[x=0.00108cm,y=0.85cm,font=\small]", ] for j, row in enumerate(rows): y = 3 - j components = [Decimal(row["derived"]["inferred_movement_time_s"]), Decimal(5 * row["measure_accepted_count"]), Decimal(row["channel_switch_count"]), Decimal(5 * row["cleared_jammer_count"] + 3 * row["clear_failure_count"])] label = f"Q{row['problem_no']}-{row['formal_index']}" bars.append(fr"\node[anchor=east] at (-100,{y + 0.3}) {{{label}}};") current = Decimal(0) for amount, color in zip(components, colors): bars.append(fr"\fill[{color}] ({current},{y}) rectangle ({current + amount},{y + 0.6});") current += amount bars.append(fr"\node[anchor=west,font=\scriptsize] at ({current + 80},{y + 0.3}) {{{current:.1f}}};") bars.append(r"\draw[->] (0,-0.25)--(10500,-0.25);") for tick in range(0, 10001, 2000): bars.append(fr"\draw ({tick},-0.25)--({tick},-0.35) node[below,font=\scriptsize] {{{tick}}};") bars.append(r"\node[anchor=west,font=\scriptsize] at (10400,-0.65) {s};") for x, color, label in zip((0, 2600, 5200, 7600), colors, ("移动", "检测", "换频", "清除")): bars += [fr"\fill[{color}] ({x},-1.3) rectangle ({x + 350},-1.05);", fr"\node[anchor=west] at ({x + 450},-1.175) {{{label}}};"] bars += [ r"\end{tikzpicture}", r"\caption{四次正式测试的累计虚拟耗时。移动项由总时间扣除官方动作计数对应耗时反推。}", r"\label{fig:costbars}\end{figure}", ] (out / "formal-time-bars.tex").write_text("\n".join(bars) + "\n", "utf-8") def listings(rows: list[dict]) -> None: data = PAPER / "data" files = sorted((ROOT / "src/mcm26b").rglob("*.py")) files += [ROOT / "scripts/audit_paper_formal.py", ROOT / "scripts/build_paper_support.py"] text = [ r"\begingroup", r"\linespread{1.0}\selectfont", r"\lstset{basicstyle=\ttfamily\fontsize{8}{10}\selectfont,numberstyle=\tiny," r"aboveskip=6pt,belowskip=8pt,xleftmargin=1.5em,frame=none}", ] for path in files: rel = path.relative_to(ROOT).as_posix() display = data / "listing-source" / (rel + ".tex") display.parent.mkdir(parents=True, exist_ok=True) display.write_text(escaped_listing(path.read_text("utf-8")), "utf-8") display_rel = display.relative_to(PAPER).as_posix() escape_option = "escapeinside={" + ESCAPE_OPEN + "}{" + ESCAPE_CLOSE + "}" text += [fr"\subsection*{{\pathtext{{{rel}}}}}", fr"\lstinputlisting[{escape_option}]{{{display_rel}}}"] text.append(r"\endgroup") (data / "source-listings.tex").write_text("\n".join(text) + "\n", "utf-8") 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", ] for row in rows: h = row["package_sha256"] label = f"Q{row['problem_no']}-{row['formal_index']}" table += [ fr"{label} & \pathtext{{{Path(row['file']).name}}}\\", 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") SUPPORT_README = """# 论文支撑材料 只引用官方模拟器 test 中的四份正式导出及哈希匹配的官方统计。 问题三、四各缺第三份正式记录。正式源总数未知,不能计算全清率。 完整原始包保留原名,匿名快照不含队号与票据内容。 ## 环境和运行 Python >=3.10;安装 requests,可选 numpy 加速。 在本目录使用 PowerShell: $env:PYTHONPATH = (Resolve-Path src).Path python -m mcm26b.cli --help python -m mcm26b.cli run --problem 3 --strategy bayes --robot-id <模拟器登录队号> python -m mcm26b.cli run --problem 4 --strategy directional --robot-id <模拟器登录队号> 在线命令需要先由使用者在官方模拟器中选择测试、待接口就绪。 重写论文未运行这些在线命令。自建评测机及历史场景不作为论文实验资料。 策略的自检命令涉及另一个可选 referee 包,本包用于正式接口路径。 ## 重算论文数据和图表 python scripts/audit_paper_formal.py --snapshot docs/paper/data/formal-results.json python scripts/build_paper_support.py 上述命令均为离线处理,不连接模拟器接口。 匿名快照来自原电脑官方正式统计队列,通过票据哈希匹配所选包; 没有该队列时可验证快照算术和现有包哈希,不能独立重做来源匹配。 源码与正式包无策略提交版本哈希绑定;不作同场景消融或全清率宣称。 31点计划只有离散证据;混合类型下的 joint_empty 判定边界见论文正文。 ## 编译 安装 TeX Live(XeLaTeX、ctex、TikZ),进入 docs/paper: latexmk -xelatex -interaction=nonstopmode -halt-on-error main.tex 版式使用原项目模板。精简支撑包不含大体积字体,编译前请将原项目 docs/paper/fonts 目录放回同一相对位置;不要修改模板以替代这些字体。 使用 latexmk 自动完成交叉引用所需的多遍编译,不能只运行一次 XeLaTeX。 support-manifest.json 列出包内全部材料(清单自身除外)及 SHA-256。 """ def package(rows: list[dict]) -> Path: selected = set((ROOT / "src/mcm26b").rglob("*.py")) selected.update((ROOT / "src/mcm26b/guarantee/plans").glob("*.json")) selected.update((PAPER / "data").rglob("*")) selected.update((PAPER / "figures").glob("*.tex")) selected.update((PAPER / "sections").glob("*.tex")) selected.update([PAPER / "main.tex", PAPER / "mcm26b-paper.cls", PAPER / "README.md", PAPER / "REWRITE_NOTES.md", ROOT / "scripts/audit_paper_formal.py", ROOT / "scripts/build_paper_support.py"]) for row in rows: path = ROOT / "Jammers-simulator-full" / row["file"] if digest(path.read_bytes()) != row["package_sha256"]: raise ValueError("Original formal package changed") selected.add(path) blobs = {p.relative_to(ROOT).as_posix(): p.read_bytes() for p in sorted(selected) if p.is_file()} blobs["README.md"] = SUPPORT_README.encode("utf-8") blobs["requirements.txt"] = b"requests>=2.31\nnumpy>=1.24\n" manifest = [{"file": name, "bytes": len(value), "sha256": digest(value)} for name, value in sorted(blobs.items())] blobs["support-manifest.json"] = (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8") out = PAPER / "support-materials.zip" with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for name, value in sorted(blobs.items()): entry = zipfile.ZipInfo(name, (2026, 9, 13, 0, 0, 0)) entry.compress_type = zipfile.ZIP_DEFLATED archive.writestr(entry, value) if out.stat().st_size > 20_000_000: raise ValueError("Support archive exceeds 20 MB") print(f"Support archive: {len(blobs)} files, {out.stat().st_size} bytes") return out 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"] figures(rows) listings(rows) if args.package: package(rows) if __name__ == "__main__": main()