485 lines
25 KiB
Python
485 lines
25 KiB
Python
"""Build vector figure sources, comparison tables, the formal-data table, and a support ZIP.
|
||
|
||
The generated table rows are f-strings whose value equals the LaTeX line to be
|
||
written; wrapping them would obscure that one-to-one correspondence, so this module
|
||
is exempt from the line-length check (the only pylint exception in scripts/).
|
||
"""
|
||
# pylint: disable=line-too-long
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import csv
|
||
import hashlib
|
||
import json
|
||
import zipfile
|
||
from decimal import Decimal, ROUND_HALF_UP
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
PAPER = ROOT / "docs/paper"
|
||
PROJECT_RESULTS = ROOT / "project/results"
|
||
|
||
|
||
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)
|
||
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}",
|
||
]
|
||
write_tex(out / "q4-plan.tex", "\n".join(lines) + "\n")
|
||
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 = len(rows) - 1 - 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}",
|
||
]
|
||
write_tex(out / "formal-time-bars.tex", "\n".join(bars) + "\n")
|
||
|
||
|
||
def fixed(value: float | None, digits: int = 3) -> str:
|
||
if value is None:
|
||
return "--"
|
||
return str(Decimal(str(value)).quantize(Decimal(1).scaleb(-digits), rounding=ROUND_HALF_UP))
|
||
|
||
|
||
def percent(value: float | None, digits: int = 1) -> str:
|
||
if value is None:
|
||
return "--"
|
||
return fixed(100 * value, digits) + r"\%"
|
||
|
||
|
||
def flag(value: bool | None) -> str:
|
||
return "--" if value is None else ("是" if value else "否")
|
||
|
||
|
||
def project_tables() -> None:
|
||
"""Generate the optimal-versus-baseline comparison tables from project/results."""
|
||
out = PAPER / "data"
|
||
out.mkdir(parents=True, exist_ok=True)
|
||
data = {name: json.loads((PROJECT_RESULTS / f"{name}.json").read_text("utf-8"))
|
||
for name in ("q1", "q2", "q3", "q4")}
|
||
examples, random = data["q1"]["aggregate_examples"], data["q1"]["aggregate_random"]
|
||
two = data["q2"]["aggregate"]
|
||
three, paired3 = data["q3"]["aggregate"]["variants"], data["q3"]["aggregate"]["paired"]
|
||
four, paired4 = data["q4"]["aggregate"]["variants"], data["q4"]["aggregate"]["paired"]
|
||
head = "% Generated by scripts/build_paper_support.py from project/results/*.json."
|
||
|
||
macros = [
|
||
("ProjQOneExampleCases", str(examples["cases"])),
|
||
("ProjQOneRandomCases", str(random["cases"])),
|
||
("ProjQOneFixedStatusBase", percent(examples["status_accuracy_baseline"])),
|
||
("ProjQOneFixedMaeBase", fixed(examples["diameter_mae_baseline_m"])),
|
||
("ProjQOneFixedMaeMaxBase", fixed(examples["diameter_max_err_baseline_m"])),
|
||
("ProjQOneFixedCoverBase", percent(examples["coverage_accuracy_baseline"])),
|
||
("ProjQOneFixedMsOpt", fixed(examples["runtime_optimal_mean_ms"])),
|
||
("ProjQOneFixedMsBase", fixed(examples["runtime_baseline_mean_ms"])),
|
||
("ProjQOneRandStatusBase", percent(random["status_accuracy_baseline"])),
|
||
("ProjQOneRandMaeBase", fixed(random["diameter_mae_baseline_m"])),
|
||
("ProjQOneRandMaeMaxBase", fixed(random["diameter_max_err_baseline_m"])),
|
||
("ProjQOneRandCoverBase", percent(random["coverage_accuracy_baseline"])),
|
||
("ProjQOneRandMsOpt", fixed(random["runtime_optimal_mean_ms"])),
|
||
("ProjQOneRandMsBase", fixed(random["runtime_baseline_mean_ms"])),
|
||
("ProjQTwoCases", str(two["optimal"]["cases"])),
|
||
("ProjQTwoInitialRadius", fixed(two["_initial_radius_mean_m"])),
|
||
("ProjQTwoGuaranteeOpt", percent(two["optimal"]["guaranteed_rate"])),
|
||
("ProjQTwoGuaranteeMec", percent(two["baseline_mec"]["guaranteed_rate"])),
|
||
("ProjQTwoGuaranteePerp", percent(two["baseline_perp"]["guaranteed_rate"])),
|
||
("ProjQTwoWorstOpt", fixed(two["optimal"]["worst_posterior_mean_m"])),
|
||
("ProjQTwoWorstRef", fixed(two["reference"]["worst_posterior_mean_m"])),
|
||
("ProjQTwoRatioOpt", fixed(two["optimal"]["final_ratio_mean"])),
|
||
("ProjQTwoRatioMec", fixed(two["baseline_mec"]["final_ratio_mean"])),
|
||
("ProjQTwoWorstPerp", fixed(two["baseline_perp"]["worst_posterior_mean_m"])),
|
||
("ProjQTwoWorstMec", fixed(two["baseline_mec"]["worst_posterior_mean_m"])),
|
||
("ProjQThreeScenes", str(three["optimal"]["episodes"])),
|
||
("ProjQThreeMeanOpt", fixed(three["optimal"]["virtual_time_mean_s"])),
|
||
("ProjQThreeMeanSeven", fixed(three["seven"]["virtual_time_mean_s"])),
|
||
("ProjQThreeMeanBase", fixed(three["baseline"]["virtual_time_mean_s"])),
|
||
("ProjQThreeDropOpt", percent(-paired3["optimal_vs_baseline"]["mean_relative_change"])),
|
||
("ProjQThreeDropMedianOpt",
|
||
percent(-paired3["optimal_vs_baseline"]["median_relative_change"])),
|
||
("ProjQThreeDropSeven", percent(-paired3["seven_vs_baseline"]["mean_relative_change"])),
|
||
("ProjQThreeFasterOpt",
|
||
f"{paired3['optimal_vs_baseline']['faster_scenes']}/{paired3['optimal_vs_baseline']['compared']}"),
|
||
("ProjQThreeMeasuresOpt", fixed(three["optimal"]["measures_mean"])),
|
||
("ProjQThreeMeasuresBase", fixed(three["baseline"]["measures_mean"])),
|
||
("ProjQThreeMoveOpt", fixed(three["optimal"]["move_distance_mean_m"] / 1000)),
|
||
("ProjQThreeMoveBase", fixed(three["baseline"]["move_distance_mean_m"] / 1000)),
|
||
("ProjQThreeWallOpt", fixed(three["optimal"]["compute_wall_mean_s"])),
|
||
("ProjQFourScenes", str(four["optimal"]["episodes"])),
|
||
("ProjQFourMeanOpt", fixed(four["optimal"]["virtual_time_mean_s"])),
|
||
("ProjQFourMeanGrid", fixed(four["baseline_grid"]["virtual_time_mean_s"])),
|
||
("ProjQFourMeanBase", fixed(four["baseline"]["virtual_time_mean_s"])),
|
||
("ProjQFourClearOpt", percent(four["optimal"]["cleared_ratio_mean"])),
|
||
("ProjQFourClearGrid", percent(four["baseline_grid"]["cleared_ratio_mean"])),
|
||
("ProjQFourClearBase", percent(four["baseline"]["cleared_ratio_mean"])),
|
||
("ProjQFourAbsentBase", str(four["baseline"]["absent_misjudged_total"])),
|
||
("ProjQFourMeasuresOpt", fixed(four["optimal"]["measures_mean"])),
|
||
("ProjQFourMeasuresBase", fixed(four["baseline"]["measures_mean"])),
|
||
("ProjQFourFallbackOpt", fixed(four["optimal"]["fallback_clear_attempts_mean"])),
|
||
("ProjQFourValidOpt",
|
||
f"{four['optimal']['valid_episodes']}/{four['optimal']['episodes']}"),
|
||
("ProjQFourValidGrid",
|
||
f"{four['baseline_grid']['valid_episodes']}/{four['baseline_grid']['episodes']}"),
|
||
("ProjQFourRiseOpt", percent(paired4["optimal_vs_baseline"]["mean_relative_change"])),
|
||
("ProjQFourMoveOpt", fixed(four["optimal"]["move_distance_mean_m"] / 1000)),
|
||
("ProjQFourMoveBase", fixed(four["baseline"]["move_distance_mean_m"] / 1000)),
|
||
]
|
||
lines = [head] + [f"\\newcommand{{\\{name}}}{{{value}}}" for name, value in macros]
|
||
write_tex(out / "project-macros.tex", "\n".join(lines) + "\n")
|
||
|
||
def write(stem: str, rows: list[str]) -> None:
|
||
write_tex(out / stem, "\n".join([head, *rows]) + "\n")
|
||
|
||
write("project-q1-summary-rows.tex", [
|
||
f"最优(固定 {examples['cases']} 例) & {percent(examples['status_accuracy_optimal'])} & "
|
||
f"{fixed(examples['diameter_mae_optimal_m'])} & {fixed(examples['diameter_max_err_optimal_m'])} & "
|
||
f"{percent(examples['coverage_accuracy_optimal'])} & {fixed(examples['runtime_optimal_mean_ms'])} " + r"\\",
|
||
f"最优(随机 {random['cases']} 例) & {percent(random['status_accuracy_optimal'])} & "
|
||
f"{fixed(random['diameter_mae_optimal_m'])} & {fixed(random['diameter_max_err_optimal_m'])} & "
|
||
f"{percent(random['coverage_accuracy_optimal'])} & {fixed(random['runtime_optimal_mean_ms'])} " + r"\\",
|
||
f"基线(固定 {examples['cases']} 例) & {percent(examples['status_accuracy_baseline'])} & "
|
||
f"{fixed(examples['diameter_mae_baseline_m'])} & {fixed(examples['diameter_max_err_baseline_m'])} & "
|
||
f"{percent(examples['coverage_accuracy_baseline'])} & {fixed(examples['runtime_baseline_mean_ms'])} " + r"\\",
|
||
f"基线(随机 {random['cases']} 例) & {percent(random['status_accuracy_baseline'])} & "
|
||
f"{fixed(random['diameter_mae_baseline_m'])} & {fixed(random['diameter_max_err_baseline_m'])} & "
|
||
f"{percent(random['coverage_accuracy_baseline'])} & {fixed(random['runtime_baseline_mean_ms'])} " + r"\\",
|
||
])
|
||
|
||
def variant_rows(table: dict, mapping: list[tuple[str, str]]) -> list[str]:
|
||
return [
|
||
f"{label} & {table[key]['valid_episodes']}/{table[key]['episodes']} & "
|
||
f"{percent(table[key]['cleared_ratio_mean'])} & {table[key]['absent_misjudged_total']} & "
|
||
f"{fixed(table[key]['virtual_time_mean_s'])} & {fixed(table[key]['virtual_time_median_s'])} & "
|
||
f"{fixed(table[key]['virtual_time_max_s'])} & {fixed(table[key]['measures_mean'], 1)} & "
|
||
f"{fixed(table[key]['move_distance_mean_m'] / 1000)} & {fixed(table[key]['fallback_clear_attempts_mean'], 1)} "
|
||
+ r"\\"
|
||
for key, label in mapping
|
||
]
|
||
|
||
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 单独生成。
|
||
|
||
论文附录只收录四问主线的核心函数(数据源为单文件核心代码 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",
|
||
]
|
||
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}"]
|
||
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 = """# 论文支撑材料
|
||
|
||
汇总自建模拟器(评测机 `referee/`)的演练局日志与官方模拟器 `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 <模拟器登录队号>
|
||
|
||
在线命令需要先由使用者在官方模拟器中选择测试、待接口就绪。
|
||
自建模拟器可用 `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 # 附录正式数据表与支撑包
|
||
|
||
第一条命令按题面计时模型复算 `referee-data/sessions/` 下的每局演练日志,
|
||
输出论文引用的宏、表格与插图数据;复算值与日志记录的最大偏差为 0.000000 s。
|
||
正式统计快照来自官方统计队列,通过票据哈希匹配所选包;
|
||
重新验证来源匹配需原电脑的统计队列。
|
||
连续覆盖与混合类型推断的扩展见论文未来工作。
|
||
|
||
## 编译
|
||
|
||
安装 TeX Live(XeLaTeX、ctex、TikZ),进入 docs/paper:
|
||
|
||
latexmk -xelatex -interaction=nonstopmode -halt-on-error main.tex
|
||
|
||
版式使用原项目模板。编译前请将原项目 docs/paper/fonts 目录放回
|
||
同一相对位置。使用 latexmk 自动完成交叉引用所需的多遍编译。
|
||
`data/*.tex` 由脚本生成,一律使用 LF 换行:CRLF 会让 `\\input` 的表格行
|
||
在 TeX 缓冲中提前 `\\par` 化,触发 booktabs 的 "Misplaced \\noalign"。
|
||
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((ROOT / "docs/figures").glob("*.png"))
|
||
selected.update((ROOT / "docs/figures").glob("*.json"))
|
||
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"]
|
||
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 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 = formal_rows()
|
||
figures(rows)
|
||
project_tables()
|
||
listings(rows)
|
||
if args.package:
|
||
package(rows)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|