论文更改与图更改
Co-authored-by: Pengheng <Horldsence@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""Audit only the formal exports selected in the official simulator's test folder.
|
||||
|
||||
No encrypted payload is opened. Match the public activation-ticket digest to the
|
||||
official statistics queue, and the complete package digest to its result sidecar.
|
||||
Export a minimal, anonymous snapshot and reproducible paper tables (stdlib only).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
import struct
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIELDS = (
|
||||
"entered", "end_reason", "cleared_jammer_count", "measure_accepted_count",
|
||||
"virtual_time_us", "program_run_duration_ms", "channel_switch_count",
|
||||
"clear_failure_count", "state",
|
||||
)
|
||||
|
||||
|
||||
def sha(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def public_header(path: Path) -> tuple[dict, str]:
|
||||
data = path.read_bytes()
|
||||
if data[:8] != b"JMBFLOG1" or len(data) < 14:
|
||||
raise ValueError(f"Unsupported formal envelope: {path.name}")
|
||||
version, size = struct.unpack(">HI", data[8:14])
|
||||
if version != 1 or not 0 < size <= len(data) - 14:
|
||||
raise ValueError(f"Invalid envelope header: {path.name}")
|
||||
header = json.loads(data[14:14 + size])
|
||||
if header["package_type"] != "formal_behavior_log":
|
||||
raise ValueError("Practice logs are not admitted")
|
||||
return header, sha(data)
|
||||
|
||||
|
||||
def fixed(value: Decimal, digits: int = 3) -> str:
|
||||
return str(value.quantize(Decimal(1).scaleb(-digits), rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def derive(row: dict) -> dict:
|
||||
count = row["cleared_jammer_count"]
|
||||
total = Decimal(row["virtual_time_us"]) / 1_000_000
|
||||
movement = total - (5 * row["measure_accepted_count"]
|
||||
+ row["channel_switch_count"] + 5 * count
|
||||
+ 3 * row["clear_failure_count"])
|
||||
if count <= 0 or movement < 0:
|
||||
raise ValueError("Invalid counts or negative inferred movement time")
|
||||
return {
|
||||
"virtual_time_s": str(total),
|
||||
"average_clear_time_s": fixed(total / count, 6),
|
||||
"program_run_duration_s": fixed(Decimal(row["program_run_duration_ms"]) / 1000),
|
||||
"inferred_movement_time_s": str(movement),
|
||||
"inferred_distance_m": str(5 * movement),
|
||||
"movement_share_percent": fixed(100 * movement / total, 6),
|
||||
"source_total": None,
|
||||
"clear_fraction": None,
|
||||
}
|
||||
|
||||
|
||||
def collect(sim: Path) -> list[dict]:
|
||||
selected = sorted((sim / "test").rglob("*.jlog"))
|
||||
if not selected:
|
||||
raise ValueError("No formal exports in test/")
|
||||
queue = sim / "JammersSimulatorData/formal-statistics-queue.sqlite3"
|
||||
original = sim / "JammersSimulatorData/behavior-logs"
|
||||
# Read the live queue including its WAL, without writing to the simulator.
|
||||
with sqlite3.connect(queue.resolve().as_uri() + "?mode=ro", uri=True) as db:
|
||||
db.row_factory = sqlite3.Row
|
||||
query = "SELECT activation_ticket_sha256," + ",".join(FIELDS) + " FROM statistics_tasks"
|
||||
statistics = {r["activation_ticket_sha256"]: dict(r) for r in db.execute(query)}
|
||||
records = []
|
||||
for path in selected:
|
||||
header, digest = public_header(path)
|
||||
encoded = header["server_ticket_b64"]
|
||||
ticket_hash = sha(base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)))
|
||||
sidecar = json.loads((original / (path.stem + ".result.json")).read_text("utf-8"))
|
||||
for field in ("problem_no", "formal_index", "case_code"):
|
||||
if header[field] != sidecar[field]:
|
||||
raise ValueError(f"Mismatched case: {path.name}")
|
||||
if digest != sidecar["package_sha256"] or digest != sha((original / path.name).read_bytes()):
|
||||
raise ValueError(f"Mismatched formal package: {path.name}")
|
||||
matched = statistics[ticket_hash]
|
||||
row = {name: header[name] for name in ("problem_no", "formal_index", "case_code", "client_version")}
|
||||
row.update({name: matched[name] for name in FIELDS})
|
||||
row.update({
|
||||
"file": path.relative_to(sim).as_posix(), "size_bytes": path.stat().st_size,
|
||||
"package_sha256": digest, "activation_ticket_sha256": ticket_hash,
|
||||
"created_at_utc": header["created_at_utc"],
|
||||
"ended_at_utc": sidecar["ended_at_utc"],
|
||||
"content_encryption": header["content_encryption"],
|
||||
"identity_and_ticket_content_omitted": True,
|
||||
"same_as_official_original": True,
|
||||
})
|
||||
row["derived"] = derive(row)
|
||||
records.append(row)
|
||||
keys = [(r["problem_no"], r["formal_index"]) for r in records]
|
||||
if len(set(keys)) != len(keys):
|
||||
raise ValueError("Duplicate formal attempt")
|
||||
return records
|
||||
|
||||
|
||||
def write_outputs(rows: list[dict], out: Path) -> None:
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
snapshot = {
|
||||
"schema": "paper-formal-audit-v1",
|
||||
"selection": "Only Jammers-simulator-full/test/**/*.jlog",
|
||||
"statistics_source": "Official formal-statistics-queue.sqlite3; matched by SHA-256 of the public activation ticket",
|
||||
"limitations": [
|
||||
"Encrypted action payloads are not decrypted; no trajectory or bearing observations are available.",
|
||||
"Source totals and omnidirectional/directional counts are not disclosed.",
|
||||
"Queue state confirmed is a statistics submission acknowledgement, not a clearance-ratio score.",
|
||||
"No solver commit or configuration digest is bound to these public statistics.",
|
||||
"The third formal attempt for each problem is not present in the selected data.",
|
||||
],
|
||||
"records": rows,
|
||||
}
|
||||
(out / "formal-results.json").write_text(json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n", "utf-8")
|
||||
columns = ["problem_no", "formal_index", "case_code", *FIELDS, "package_sha256"]
|
||||
with (out / "formal-results.csv").open("w", encoding="utf-8-sig", newline="") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=columns + list(rows[0]["derived"]))
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
writer.writerow({**{key: row[key] for key in columns}, **row["derived"]})
|
||||
for problem in (3, 4):
|
||||
by_index = {r["formal_index"]: r for r in rows if r["problem_no"] == problem}
|
||||
lines = ["% Generated by scripts/audit_paper_formal.py; seconds; half-up rounding."]
|
||||
for index in (1, 2, 3):
|
||||
row = by_index.get(index)
|
||||
if row is None:
|
||||
lines.append(f"测试 {index} & 未提供记录 & -- & -- & -- " + r"\\")
|
||||
else:
|
||||
avg = Decimal(row["virtual_time_us"]) / 1_000_000 / row["cleared_jammer_count"]
|
||||
lines.append(f"测试 {index} & \\texttt{{{row['case_code']}}} & {row['cleared_jammer_count']} & {fixed(avg)} & {row['derived']['program_run_duration_s']} " + r"\\")
|
||||
(out / f"formal-q{problem}-rows.tex").write_text("\n".join(lines) + "\n", "utf-8")
|
||||
cost_lines, constants = [], []
|
||||
for row in rows:
|
||||
label = f"Q{row['problem_no']}-{row['formal_index']}"
|
||||
total = Decimal(row["virtual_time_us"]) / 1_000_000
|
||||
move = Decimal(row["derived"]["inferred_movement_time_s"])
|
||||
cost_lines.append(f"{label} & {row['measure_accepted_count']} & {row['channel_switch_count']} & {row['clear_failure_count']} & {fixed(total)} & {fixed(move)} & {fixed(100*move/total, 2)}\\% " + r"\\")
|
||||
(out / "formal-cost-rows.tex").write_text("\n".join(cost_lines) + "\n", "utf-8")
|
||||
for problem, word in ((3, "Three"), (4, "Four")):
|
||||
group = [r for r in rows if r["problem_no"] == problem]
|
||||
count = sum(r["cleared_jammer_count"] for r in group)
|
||||
total = sum(Decimal(r["virtual_time_us"]) / 1_000_000 for r in group)
|
||||
mean = sum(Decimal(r["virtual_time_us"]) / 1_000_000 / r["cleared_jammer_count"] for r in group) / len(group)
|
||||
constants.extend([f"\\newcommand{{\\Q{word}Count}}{{{count}}}",
|
||||
f"\\newcommand{{\\Q{word}Weighted}}{{{fixed(total/count)}}}",
|
||||
f"\\newcommand{{\\Q{word}Mean}}{{{fixed(mean)}}}"])
|
||||
(out / "formal-macros.tex").write_text("\n".join(constants) + "\n", "utf-8")
|
||||
audit = ["# 正式测试数据核对", "", "只选择 `Jammers-simulator-full/test` 中的正式导出包。",
|
||||
"加密载荷不作解密。公开头部提供案例与票据;包哈希核对原件及 result 侧文件;票据哈希匹配官方正式统计数据库。",
|
||||
"导出快照不包含队号、设备信息、票据内容、密钥封装或账号信息。", "",
|
||||
"| 案例 | 正式序号 | 原件一致 | 包 SHA-256 |", "| --- | --- | --- | --- |"]
|
||||
for row in rows:
|
||||
audit.append(f"| {row['case_code']} | Q{row['problem_no']}-{row['formal_index']} | 是 | `{row['package_sha256']}` |")
|
||||
audit += ["", "虚拟时间保留官方整数微秒,程序运行时间采用官方毫秒。平均定位清除时间 = 总虚拟秒数 / 清除数。",
|
||||
"移动时间由总时长减去检测、换频、成功及失败清除耗时反推;不是从解密轨迹逐动作复算所得。",
|
||||
"`confirmed` 仅指统计提交确认,`user_exit` 仅指主动退出。源总数未知,不能报告全清率。",
|
||||
"问题三、四均缺第三次正式导出;不以演练或自建模拟器结果替代。",
|
||||
"当前算法描述对应仓库代码;公开统计没有策略提交版本标识,不能用这四局作当前参数的配对消融。", ""]
|
||||
(out / "audit.md").write_text("\n".join(audit), "utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--sim-root", type=Path, default=ROOT / "Jammers-simulator-full")
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "docs/paper/data")
|
||||
parser.add_argument("--snapshot", type=Path, help="Regenerate tables from the audited anonymous snapshot, without the simulator database")
|
||||
args = parser.parse_args()
|
||||
if args.snapshot:
|
||||
rows = json.loads(args.snapshot.read_text("utf-8"))["records"]
|
||||
for row in rows:
|
||||
if derive(row) != row["derived"]:
|
||||
raise ValueError("Snapshot arithmetic changed")
|
||||
path = args.sim_root / row["file"]
|
||||
if path.exists() and public_header(path)[1] != row["package_sha256"]:
|
||||
raise ValueError("Snapshot and formal export disagree")
|
||||
else:
|
||||
rows = collect(args.sim_root)
|
||||
write_outputs(rows, args.out)
|
||||
for row in rows:
|
||||
print(f"Q{row['problem_no']}-{row['formal_index']} {row['case_code']}: "
|
||||
f"cleared={row['cleared_jammer_count']} average={row['derived']['average_clear_time_s']} s "
|
||||
f"real={row['derived']['program_run_duration_s']} s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,231 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user