yes fuking shit

This commit is contained in:
Horldsence
2026-09-12 14:48:43 +08:00
parent 61fd22598a
commit 45e48d4c91
64 changed files with 11540 additions and 57 deletions
+229
View File
@@ -0,0 +1,229 @@
"""运行历史计时分解:对任意会话日志按官方计时模型独立复算并分相。
支持两种格式(自动识别):
- 参赛队客户端日志:``scripts/capture_practice.py`` / ``SimulatorClient`` 写出的
JSONL,逐行 ``attempt`` / ``accepted`` / ``step``;
- 评测机日志:``referee-data/sessions/<case>/log.jsonl``,逐行
``session_start`` / ``action`` / ``session_end``。
复算口径与 ``scripts/verify_q3_practice.py::timing_rows`` 完全一致(题面附件2 §4.1):
``T = Σ 距离/5 + 5·N_measure + N_switch + 5·N_clear⁺ + 3·N_clear⁻``,
起点位置 (0,0)、起始频道 1,``/clear`` 不计换频,移动按整数微秒累计。
用途:把"虚拟时间花在哪"变成一条可随时重跑的命令,而不是一次性的分析结论。
uv run python scripts/analyze_run_timing.py session.jsonl
uv run python scripts/analyze_run_timing.py referee-data/sessions/*/log.jsonl --json out.json
uv run python scripts/analyze_run_timing.py session.jsonl --plan seven # 站点/探点分类
"""
from __future__ import annotations
import argparse
import json
import math
from collections import Counter, defaultdict
from pathlib import Path
SPEED_MPS = 5.0 # [题面] 附件1 表1
MEASURE_S = 5 # [题面]
SWITCH_S = 1 # [题面]
CLEAR_SUCCESS_S = 5 # [题面]
CLEAR_FAIL_S = 3 # [题面]
START_POSITION = (0.0, 0.0)
START_CHANNEL = 1
def load_actions(path: Path) -> list[dict]:
"""读取一份 JSONL,归一化为已接受动作序列。"""
actions: list[dict] = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
event = record.get("event")
if event == "accepted":
request, response = record.get("request", {}), record.get("response", {})
accepted = bool(response.get("accepted", True))
elif event == "action":
request, response = record.get("request", {}), record.get("response", {})
accepted = bool(response.get("accepted", record.get("status") == 200))
else:
continue
if not accepted:
continue
actions.append({
"path": record.get("path") or request.get("path") or "",
"position": request.get("position"),
"channel": request.get("channel"),
"result": response.get("measure_result") or response.get("clear_result") or "",
"recorded_s": response.get("virtual_time_s"),
"elapsed_s": record.get("elapsed_s"),
})
return actions
def recompute(actions: list[dict]) -> tuple[list[dict], dict]:
"""按官方模型逐动作复算;返回明细与汇总(微秒整数累计)。"""
position, channel, virtual_us = START_POSITION, START_CHANNEL, 0
rows: list[dict] = []
totals = Counter()
distance_total = 0.0
max_gap_us = 0
for index, action in enumerate(actions):
path = action["path"]
move_s = switches = action_s = 0.0
if path in ("/measure", "/clear") and action["position"] is not None:
target = (float(action["position"]["x"]), float(action["position"]["y"]))
distance_total += math.dist(position, target)
move_s = math.dist(position, target) / SPEED_MPS
position = target
if path == "/measure":
switches = int(channel != action["channel"])
channel = action["channel"]
action_s = MEASURE_S
else:
action_s = CLEAR_SUCCESS_S if action["result"] == "success" else CLEAR_FAIL_S
move_us = round(move_s * 1_000_000)
virtual_us += move_us + int((switches + action_s) * 1_000_000)
totals["move_us"] += move_us
totals["switch_s"] += switches
totals["action_us"] += int(action_s * 1_000_000)
totals["count"] += 1
if action["elapsed_s"]:
totals["elapsed_s"] += action["elapsed_s"]
recorded = action["recorded_s"]
gap_us = ((round(recorded * 1_000_000) - virtual_us)
if isinstance(recorded, (int, float)) else 0)
max_gap_us = max(max_gap_us, abs(gap_us))
rows.append({"index": index, "path": path, "channel": action["channel"],
"x": position[0], "y": position[1], "result": action["result"],
"move_us": move_us, "switch_s": switches, "action_s": action_s,
"recomputed_us": virtual_us, "recorded_s": recorded, "gap_us": gap_us})
summary = {
"actions": len(actions),
"virtual_s": round(virtual_us / 1_000_000, 6),
"move_s": round(totals["move_us"] / 1_000_000, 3),
"switch_s": float(totals["switch_s"]),
"action_s": round(totals["action_us"] / 1_000_000, 3),
"distance_m": round(distance_total, 1),
"real_elapsed_s": round(totals["elapsed_s"], 3),
"max_recorded_gap_s": max_gap_us / 1_000_000,
}
return rows, summary
def phase_report(rows: list[dict]) -> dict:
"""清除前后分相 + 重复复扫检测(定位"时间被重复动作吃掉"的历史问题)。"""
clear_indices = [r["index"] for r in rows if r["path"] == "/clear"]
last_clear = max(clear_indices) if clear_indices else None
measure_keys = defaultdict(int)
for row in rows:
if row["path"] == "/measure":
key = (round(row["x"] * 2), round(row["y"] * 2), row["channel"])
measure_keys[key] += 1
repeated = sum(count - 1 for count in measure_keys.values() if count > 1)
report = {
"measures": sum(1 for r in rows if r["path"] == "/measure"),
"clears": len(clear_indices),
"failed_clears": sum(1 for r in rows
if r["path"] == "/clear" and r["result"] != "success"),
"distinct_measure_stops": len(measure_keys),
"repeated_measure_actions": repeated,
"repeat_ratio": round(repeated / max(1, len(measure_keys)), 3),
}
if last_clear is not None:
tail = [r for r in rows if r["index"] > last_clear]
report["last_clear_virtual_s"] = round(rows[last_clear]["recomputed_us"] / 1_000_000, 3)
report["after_last_clear_virtual_s"] = round(
sum(r["move_us"] + (r["switch_s"] + r["action_s"]) * 1_000_000 for r in tail)
/ 1_000_000, 3)
report["after_last_clear_measures"] = sum(1 for r in tail if r["path"] == "/measure")
return report
def classify_by_plan(rows: list[dict], points, tol_m: float = 2.0) -> dict:
"""按给定站点集把 /measure 分成"站点扫频"与"专用探点",并给出各自移动耗时。"""
buckets: dict[str, Counter] = defaultdict(Counter)
for row in rows:
if row["path"] != "/measure":
continue
nearest = min((math.dist((row["x"], row["y"]), p) for p in points), default=math.inf)
kind = "station" if nearest <= tol_m else "probe"
buckets[kind]["n"] += 1
buckets[kind]["move_us"] += row["move_us"]
return {kind: {"measures": value["n"],
"move_s": round(value["move_us"] / 1_000_000, 3)}
for kind, value in buckets.items()}
def _plan_points(name: str):
if name == "none":
return None
if name == "seven":
from mcm26b.planner.seven import seven_points
return seven_points()
if name == "49":
from mcm26b.coverage import coverage_points
return coverage_points()
raise ValueError(f"未知 --plan: {name}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("logs", nargs="+", type=Path, help="会话 JSONL(客户端或评测机格式)")
parser.add_argument("--plan", choices=("none", "seven", "49"), default="none",
help="站点集,用于把 /measure 分成站点扫频与专用探点")
parser.add_argument("--json", type=Path, default=None, help="汇总输出路径")
parser.add_argument("--top", type=int, default=6, help="逐频道表格显示前 N 个热点频道")
args = parser.parse_args()
points = _plan_points(args.plan)
payload = {}
for path in args.logs:
actions = load_actions(path)
if not actions:
print(f"[跳过] {path}:没有已接受动作")
continue
rows, summary = recompute(actions)
report = phase_report(rows)
total = summary["virtual_s"] or 1.0
print(f"\n===== {path} =====")
print(f"虚拟总时间 {summary['virtual_s']:.3f} s "
f"移动 {summary['move_s']:.1f} s ({100 * summary['move_s'] / total:.1f}%) "
f"检测/清除动作 {summary['action_s']:.1f} s ({100 * summary['action_s'] / total:.1f}%) "
f"换频 {summary['switch_s']:.0f} s ({100 * summary['switch_s'] / total:.1f}%)")
print(f"移动距离 {summary['distance_m']:.1f} m 请求 {summary['actions']} 次 "
f"现实耗时 {summary['real_elapsed_s']:.3f} s "
f"与服务器记录最大偏差 {summary['max_recorded_gap_s']:.6f} s")
print(f"检测 {report['measures']} 清除 {report['clears']}(失败 {report['failed_clears']}) "
f"不同测点 {report['distinct_measure_stops']} "
f"重复检测 {report['repeated_measure_actions']}"
f"(重复率 {report['repeat_ratio']})")
if "last_clear_virtual_s" in report:
print(f"末次清除于 {report['last_clear_virtual_s']:.1f} s;其后又花 "
f"{report['after_last_clear_virtual_s']:.1f} s 做 "
f"{report['after_last_clear_measures']} 次检测")
per_channel = Counter(r["channel"] for r in rows if r["path"] == "/measure")
hottest = per_channel.most_common(args.top)
print("检测次数最多的频道:" + ",".join(f"ch{c}:{n}" for c, n in hottest))
if points is not None:
print("测点分类:" + json.dumps(classify_by_plan(rows, points),
ensure_ascii=False))
payload[str(path)] = {"summary": summary, "phases": report,
"per_channel_measures": dict(sorted(per_channel.items()))}
if args.json is not None:
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
print(f"\n汇总已写入 {args.json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+361
View File
@@ -0,0 +1,361 @@
"""冻结基准集的配对 runner:并行执行、逐局留档、有效性判定与配对统计。
取代散落的 benchmark_*.py 口径分叉(docs/第三四问架构优化与运行提速方案.md §P1-3):
- 场景来自 :mod:`mcm26b.harness.suites`(版本化清单,真值只在测试端);
- 每个 (场景, 变体) 一局,落 manifest / results / aggregate(.json/.csv);
- 有效性定义与既有脚本一致且不得放宽:
``全清 ∧ 缺席无误判 ∧ 正常退出 ∧ 零异常 ∧ 逐微秒计时复算一致``;
- 任何一局无效即 ``all_valid=false`` 且进程非零退出(失败不静默丢样本);
- 变体支持 ``名称@计划名`` 语法,用于同场配对比较两个覆盖计划;
- ``--jobs`` 走进程池并行(每进程独立内存回环,不共享端口)。
uv run python scripts/run_suite.py --problem 3 --suite core --jobs 6
uv run python scripts/run_suite.py --problem 4 --suite all --jobs 6 \
--variants directional@square49,directional@q4_mincover_v1 \
--label q4-plan-ab
"""
from __future__ import annotations
import argparse
import csv
import json
import statistics
from concurrent.futures import ProcessPoolExecutor
from dataclasses import replace
from pathlib import Path
from time import perf_counter
from mcm26b.config import ProblemConfig
from mcm26b.evaluation import recompute_virtual_time
from mcm26b.harness import LocalSimTransport, LocalSimulator
from mcm26b.harness.suites import (SUITE_VERSION, SceneSpec, suite,
suite_manifest)
from mcm26b.planner import DirectionalExtension
from mcm26b.planner.bayes_loop import BayesLoop
from mcm26b.planner.seven import SevenBatch
from mcm26b.protocol.client import SimulatorClient
from mcm26b.routing import route_metrics
ROOT = Path(__file__).resolve().parents[1]
Q3_STRATEGIES = {"bayes": BayesLoop, "seven_batch": SevenBatch,
"seven_immediate": SevenBatch}
Q4_STRATEGIES = {"directional": DirectionalExtension}
UNDER_TARGET_S = 4000.0
def build_config(margin_deg: float, q4_plan: str | None,
bayes_overrides: dict | None = None,
search_overrides: dict | None = None) -> ProblemConfig:
"""基准配置:persistent 关闭(对齐"首次证明找全"口径)。"""
base = ProblemConfig()
search = replace(base.search, bearing_rounding_margin_deg=margin_deg,
persistent_search=False)
if q4_plan:
search = replace(search, q4_coverage_plan=q4_plan)
if search_overrides:
search = replace(search, **search_overrides)
bayes = base.bayes
if bayes_overrides:
bayes = replace(bayes, **bayes_overrides)
return replace(base, search=search, bayes=bayes)
def _coerce(text: str):
"""变体覆盖值的字面量解析(true/false/整数/浮点/元组/字符串)。"""
lowered = text.lower()
if lowered in ("true", "yes", "on"):
return True
if lowered in ("false", "no", "off"):
return False
if text.startswith("(") and text.endswith(")"):
inner = text[1:-1].strip()
return () if not inner else tuple(
_coerce(chunk.strip()) for chunk in inner.split(","))
for cast in (int, float):
try:
return cast(text)
except ValueError:
continue
return text
def parse_variant(spec: str, problem: int) -> tuple[str, str, str | None, dict, dict]:
"""``名称[@计划][#键=值;键=值]`` → (标签, 策略名, 计划, bayes 覆盖, search 覆盖)。
变体覆盖用于同场配对比较单个开关,例如
``bayes#fuse_localize_into_tour=false``;``search.`` 前缀写入 SearchConfig。
"""
body, _, override_text = spec.partition("#")
name, _, plan = body.partition("@")
strategies = Q3_STRATEGIES if problem == 3 else Q4_STRATEGIES
if name not in strategies:
raise ValueError(f"未知策略 {name!r}(可选 {sorted(strategies)})")
bayes_overrides: dict = {}
search_overrides: dict = {}
for item in filter(None, (chunk.strip() for chunk in override_text.split(";"))):
key, _, value = item.partition("=")
if not value:
raise ValueError(f"变体覆盖须为 键=值:{item!r}")
if key.startswith("search."):
search_overrides[key[len("search."):]] = _coerce(value)
else:
bayes_overrides[key] = _coerce(value)
label = spec.replace(" ", "")
return label, name, plan or None, bayes_overrides, search_overrides
def run_episode(payload: dict) -> dict:
"""执行一局并返回结果行(进程池 worker,必须可 pickle)。"""
spec = SceneSpec(**payload["scene"])
label = payload["label"]
problem = spec.problem
config = build_config(payload["margin_deg"], payload["plan"],
payload.get("bayes"), payload.get("search"))
strategies = Q3_STRATEGIES if problem == 3 else Q4_STRATEGIES
loop_cls = strategies[payload["strategy"]]
sources = spec.sources(config)
simulator = LocalSimulator(sources, robot_id="SUITE", config=config,
error_seed=spec.error_seed)
client = SimulatorClient("SUITE", config=config,
transport=LocalSimTransport(simulator))
setup_start = perf_counter()
loop = loop_cls(client, config)
setup_wall_s = perf_counter() - setup_start
start = perf_counter()
summary = loop.run()
compute_wall_s = perf_counter() - start
report = recompute_virtual_time(client.log.action_events())
truth = sorted(source.channel for source in sources)
cleared = sorted(summary["cleared_channels"])
absent = sorted(summary["absent_channels"])
valid = (cleared == truth
and not (set(absent) & set(truth))
and summary["exit_reason"] == "user_exit"
and not summary["anomalies"]
and not report["mismatches"])
routing = _routing_metrics(summary, config)
return {
"scene_id": spec.scene_id, "variant": label, "problem": problem,
"kind": spec.kind, "sources": spec.count, "directional": spec.directional,
"virtual_time_s": summary["virtual_time_s"],
"accounted_virtual_time_s": summary["accounted_virtual_time_s"],
"measures": summary["measures"], "clears": summary["clears"],
"clear_success": summary["clear_success"],
"total_requests": summary["total_requests"],
"move_distance_m": summary["move_distance_m"],
"coverage_points": summary["coverage_points"],
"fallback_clear_attempts": summary.get("fallback_clear_attempts", 0),
"redundant_rescan_measures": summary["redundant_rescan_measures"],
"completion_basis": summary["completion_basis"],
"stop_reason": summary["stop_reason"], "exit_reason": summary["exit_reason"],
"cleared_matches_truth": cleared == truth,
"absent_misjudged": sorted(set(absent) & set(truth)),
"anomaly_count": len(summary["anomalies"]),
"virtual_recompute_mismatches": len(report["mismatches"]),
"compute_wall_s": round(compute_wall_s, 3),
"plan_verify_wall_s": round(setup_wall_s, 3),
"valid": valid,
**routing,
}
def _routing_metrics(summary: dict, config: ProblemConfig) -> dict:
"""S1 必报路由列:作业点去重、离线最优开放回路、MST 下界、缺口与探测静默率。
作业点 = 发生过 /measure 或 /clear 的位置(去重到 0.5 m);离线最优是
"同一批作业点"上的开放路径最优值,用来把"排线不够好"与"点选得不对"分开。
"""
rows = summary.get("steps") or []
positions = []
for row in rows:
position = row.get("position")
if isinstance(position, dict):
positions.append((float(position["x"]), float(position["y"])))
elif position is not None:
positions.append((float(position[0]), float(position[1])))
speed = config.cost.speed_mps
metrics = route_metrics(positions, summary["move_distance_m"], speed)
measures = [row for row in rows if row.get("action") == "measure"]
silent = sum(1 for row in measures if row.get("result") == "no_signal")
metrics["probe_no_signal_ratio"] = (
round(silent / len(measures), 4) if measures else 0.0)
plan_points = summary.get("coverage_plan_points") or []
if plan_points:
visited = 0
for (px, py) in plan_points:
if any(abs(px - x) <= 1.0 and abs(py - y) <= 1.0
for x, y in positions):
visited += 1
metrics["coverage_actual_visited"] = visited
else:
metrics["coverage_actual_visited"] = 0
return metrics
def _percentile(values: list[float], fraction: float) -> float:
if not values:
return 0.0
ordered = sorted(values)
index = min(len(ordered) - 1, max(0, int(round(fraction * (len(ordered) - 1)))))
return ordered[index]
def aggregate(rows: list[dict], labels: list[str]) -> dict:
"""逐变体汇总 + 相对首变体的配对统计。"""
summary: dict[str, dict] = {}
for label in labels:
part = [row for row in rows if row["variant"] == label]
if not part:
continue
times = [row["virtual_time_s"] for row in part]
summary[label] = {
"episodes": len(part),
"valid_episodes": sum(1 for row in part if row["valid"]),
"virtual_time_mean_s": round(statistics.fmean(times), 1),
"virtual_time_median_s": round(statistics.median(times), 1),
"virtual_time_max_s": round(max(times), 1),
"virtual_time_p90_s": round(_percentile(times, 0.9), 1),
"under_4000_ratio": round(
sum(1 for t in times if t < UNDER_TARGET_S) / len(times), 3),
"measures_mean": round(statistics.fmean(
[row["measures"] for row in part]), 1),
"total_requests_mean": round(statistics.fmean(
[row["total_requests"] for row in part]), 1),
"move_distance_mean_m": round(statistics.fmean(
[row["move_distance_m"] for row in part]), 1),
"fallback_attempts_mean": round(statistics.fmean(
[row["fallback_clear_attempts"] for row in part]), 1),
"compute_wall_mean_s": round(statistics.fmean(
[row["compute_wall_s"] for row in part]), 3),
"plan_verify_wall_mean_s": round(statistics.fmean(
[row["plan_verify_wall_s"] for row in part]), 3),
"distinct_service_points_mean": round(statistics.fmean(
[row["distinct_service_points"] for row in part]), 1),
"route_gap_vs_offline_mean": round(statistics.fmean(
[row["route_gap_vs_offline"] for row in part]), 4),
"route_lower_bound_mean_s": round(statistics.fmean(
[row["route_lower_bound_s"] for row in part]), 1),
"coverage_actual_visited_mean": round(statistics.fmean(
[row["coverage_actual_visited"] for row in part]), 1),
"probe_no_signal_ratio_mean": round(statistics.fmean(
[row["probe_no_signal_ratio"] for row in part]), 4),
}
paired: dict[str, dict] = {}
if len(labels) >= 2:
base = labels[0]
base_rows = {row["scene_id"]: row for row in rows
if row["variant"] == base}
for label in labels[1:]:
deltas, faster = [], 0
for row in rows:
if row["variant"] != label or row["scene_id"] not in base_rows:
continue
reference = base_rows[row["scene_id"]]["virtual_time_s"]
if reference <= 0:
continue
deltas.append(row["virtual_time_s"] / reference - 1.0)
if row["virtual_time_s"] < reference:
faster += 1
if deltas:
paired[f"{label}_vs_{base}"] = {
"mean_relative_change": round(statistics.fmean(deltas), 4),
"median_relative_change": round(statistics.median(deltas), 4),
"faster_scenes": faster, "compared": len(deltas)}
return {"variants": summary, "paired": paired}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--problem", type=int, choices=(3, 4), default=3)
parser.add_argument("--suite", choices=("core", "boundary", "all"), default="core")
parser.add_argument("--variants", default=None,
help="逗号分隔;支持 名称@覆盖计划,默认取该问题的标准变体")
parser.add_argument("--q4-plan", default=None, help="第四问覆盖计划(所有变体默认值)")
parser.add_argument("--bearing-margin-deg", type=float, default=0.005)
parser.add_argument("--bayes-json", default=None,
help="BayesConfig 覆盖参数(JSON 字符串或文件路径)")
parser.add_argument("--jobs", type=int, default=1, help="并行进程数")
parser.add_argument("--label", default="run", help="输出子目录名")
parser.add_argument("--out", type=Path, default=None,
help="输出根目录,默认 output/suite/")
args = parser.parse_args()
default_variants = "bayes,seven_batch" if args.problem == 3 else "directional"
variants = [parse_variant(item.strip(), args.problem)
for item in (args.variants or default_variants).split(",") if item.strip()]
labels = [label for label, *_ in variants]
bayes_overrides = None
if args.bayes_json:
raw = (Path(args.bayes_json).read_text(encoding="utf-8")
if Path(args.bayes_json).exists() else args.bayes_json)
bayes_overrides = json.loads(raw)
print(f"bayes 覆盖参数:{json.dumps(bayes_overrides, ensure_ascii=False)}")
scenes = suite(args.suite, args.problem)
out_dir = (args.out or (ROOT / "output/suite")) / f"q{args.problem}-{args.label}"
out_dir.mkdir(parents=True, exist_ok=True)
manifest = suite_manifest(args.suite, args.problem)
manifest.update({"suite_version": SUITE_VERSION, "label": args.label,
"variants": list(
(args.variants or default_variants).split(",")),
"jobs": args.jobs,
"bearing_margin_deg": args.bearing_margin_deg,
"q4_plan": args.q4_plan})
(out_dir / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
payloads = [
{"scene": scene.as_dict(), "label": label, "strategy": strategy,
"plan": plan or (args.q4_plan if args.problem == 4 else None),
"bayes": {**(bayes_overrides or {}), **variant_bayes},
"search": variant_search,
"margin_deg": args.bearing_margin_deg}
for scene in scenes
for label, strategy, plan, variant_bayes, variant_search in variants]
print(f"基准集 {args.suite}/q{args.problem}:{len(scenes)} 场景 × "
f"{len(variants)} 变体 = {len(payloads)} 局,jobs={args.jobs}", flush=True)
start = perf_counter()
if args.jobs > 1:
with ProcessPoolExecutor(max_workers=args.jobs) as pool:
rows = list(pool.map(run_episode, payloads))
else:
rows = [run_episode(payload) for payload in payloads]
wall = perf_counter() - start
rows.sort(key=lambda row: (row["scene_id"], row["variant"]))
for row in rows:
print(f" {row['scene_id']:28s} {row['variant']:34s} "
f"T={row['virtual_time_s']:9.1f} M={row['measures']:4d} "
f"wall={row['compute_wall_s']:6.2f}s "
f"{'OK' if row['valid'] else 'INVALID'}", flush=True)
summary = aggregate(rows, labels)
summary["all_valid"] = all(row["valid"] for row in rows)
summary["wall_seconds"] = round(wall, 2)
summary["episodes"] = len(rows)
summary["suite"] = args.suite
summary["problem"] = args.problem
summary["label"] = args.label
payload = {"summary": summary, "rows": rows}
(out_dir / "results.json").write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(out_dir / "aggregate.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
with (out_dir / "aggregate.csv").open("w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
header = ["scene_id", "variant", "problem", "kind", "sources", "directional"]
header += [key for key in rows[0] if key not in header]
writer.writerow(header)
for row in rows:
writer.writerow([row.get(key, "") for key in header])
print(json.dumps(summary, ensure_ascii=False, indent=2))
print(f"\n结果已写入 {out_dir}")
return 0 if summary["all_valid"] else 1
if __name__ == "__main__":
raise SystemExit(main())
+241
View File
@@ -0,0 +1,241 @@
"""第四问覆盖证书与最小命中集搜索(CLI;核心实现见 mcm26b.guarantee.cover)。
第四问的定向源在"读点位于源的前向半平面内"时才可能被检出,因此
"某频道在所有被访点都 no_signal ⟹ 该频道不存在源"的缺席证明,等价于要求
被访点集合 S 满足
∀ p ∈ Ω, ∀ h ∈ [0,360): ∃ c ∈ S,
|c − p| ≤ r_eff_min(1000) 且 (c − p)·u(h) ≥ 0
(题面规定朝向覆盖"两侧各 90 度,含边界",故取 ≥ 0)。现役 49 点方格的
"单元对角 ≤ 1000"是**充分**条件但远非必要;本脚本用贪心击中集求等价覆盖的
更小点集,并输出可复现证书。
uv run python scripts/verify_q4_cover.py # 校核 + 搜索 + 打印
uv run python scripts/verify_q4_cover.py --spacing 200 --write-plan q4_mincover_v1
uv run python scripts/verify_q4_cover.py --check-json my_points.json
"""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from mcm26b.config import ProblemConfig
from mcm26b.guarantee import (CERTIFY_HEADINGS, CERTIFY_STEP_M, R_EFF_MIN_M,
CoverageCertificate, CoveragePlan,
candidate_lattice, coverage_masks, forward_targets,
greedy_cover, omega_positions, plan_names,
square_49_plan, verify_full_disk,
verify_forward_half_disk, verify_plan, write_plan)
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_PLAN_DIR = ROOT / "src/mcm26b/guarantee/plans"
def _report(points, kind: str, step_m: float, headings: int,
r_eff: float = R_EFF_MIN_M,
heading_margin_deg: float = 0.0) -> dict:
"""按覆盖语义校核点集,返回证书字典。"""
if kind == "forward_half_disk":
certificate = verify_forward_half_disk(points, step_m, headings,
r_eff=r_eff,
heading_margin_deg=heading_margin_deg)
else:
certificate = verify_full_disk(points, step_m, r_eff)
return certificate.as_dict()
def prune_redundant(masks: list[int], chosen: list[int]) -> list[int]:
"""贪心结果冗余剪枝:删掉任何"其余点已完全覆盖其目标"的点。"""
full = 0
for index in chosen:
full |= masks[index]
pruned = list(chosen)
changed = True
while changed:
changed = False
for index in list(pruned):
rest = 0
for other in pruned:
if other != index:
rest |= masks[other]
if rest == full:
pruned.remove(index)
changed = True
return pruned
def fine_prune(points, step_m: float, headings: int, r_eff: float,
heading_margin_deg: float):
"""证书分辨率上的冗余剪枝:删掉任何点后校核仍 0 缺口才真删。
粗网格(搜索用)上的冗余判据偏保守,会在证书分辨率下留下可删的点;
本步每个候选删除都用**证书同一口径**复核,因此不削弱证书。
"""
points = list(points)
improved = True
while improved:
improved = False
for index in range(len(points)):
trial = points[:index] + points[index + 1:]
certificate = verify_forward_half_disk(
trial, step_m, headings, r_eff=r_eff,
heading_margin_deg=heading_margin_deg)
if certificate.uncovered == 0:
points = trial
improved = True
print(f" 精细剪枝:删点后仍 0 缺口,剩 {len(points)} 点", flush=True)
break
return points
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--spacing", type=float, default=200.0,
help="贪心候选点网格间距(米)")
parser.add_argument("--radius", type=float, default=2850.0,
help="候选点所在圆盘半径(米)")
parser.add_argument("--target-step", type=float, default=CERTIFY_STEP_M,
help="搜索用采样位置步长(米)")
parser.add_argument("--headings", type=int, default=CERTIFY_HEADINGS,
help="搜索用采样朝向档数")
parser.add_argument("--verify-step", type=float, default=None,
help="证书校核位置步长(默认等于 --target-step)")
parser.add_argument("--verify-headings", type=int, default=None,
help="证书校核朝向档数(默认等于 --headings)")
parser.add_argument("--design-radius", type=float, default=None,
help="搜索时的设计距离(默认 1000;<1000 即留裕量)")
parser.add_argument("--heading-margin", type=float, default=0.0,
help="搜索时的前向角裕量(度)")
parser.add_argument("--verify-radius", type=float, default=None,
help="证书校核距离(默认 1000;<1000 即留裕量)")
parser.add_argument("--verify-heading-margin", type=float, default=0.0,
help="证书校核前向角裕量(度);应 ≥ 180/verify_headings/2")
parser.add_argument("--no-prune", action="store_true", help="关闭冗余剪枝")
parser.add_argument("--fine-prune", action="store_true",
help="在证书分辨率上再做一轮冗余剪枝(删点后仍 0 缺口才删)")
parser.add_argument("--coverage", choices=("forward_half_disk", "full_disk"),
default="forward_half_disk", help="覆盖语义")
parser.add_argument("--check-json", type=Path, default=None,
help="只校核给定点集([[x,y],...] 或 {\"points\":[...]})")
parser.add_argument("--write-plan", default=None,
help="把贪心最小集写成覆盖计划(名称,落 src/mcm26b/guarantee/plans/)")
parser.add_argument("--out", type=Path, default=None, help="证书 JSON 输出路径")
args = parser.parse_args()
verify_step = (args.verify_step if args.verify_step is not None
else args.target_step)
verify_headings = (args.verify_headings if args.verify_headings is not None
else args.headings)
design_radius = (args.design_radius if args.design_radius is not None
else R_EFF_MIN_M)
verify_radius = (args.verify_radius if args.verify_radius is not None
else R_EFF_MIN_M)
if args.check_json is not None:
payload = json.loads(args.check_json.read_text(encoding="utf-8"))
raw = payload.get("points") if isinstance(payload, dict) else payload
points = [tuple(map(float, p)) for p in raw]
report = _report(points, args.coverage, verify_step, verify_headings,
verify_radius, args.verify_heading_margin)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if report["uncovered"] == 0 else 1
shipped = square_49_plan()
shipped_report = _report(shipped.points, "forward_half_disk",
verify_step, verify_headings,
verify_radius, args.verify_heading_margin)
print(f"目标空间:{shipped_report['targets']} 个 (p,h)"
f"(校核位置步长 {verify_step} m,朝向 {verify_headings} 档,"
f"r_verify={verify_radius:.0f} m,角裕量 "
f"{args.verify_heading_margin}°)")
print(f"现役 {shipped.name}:{len(shipped.points)} 点,"
f"缺口 {shipped_report['uncovered']},"
f"NN 巡回 {shipped_report['nn_tour_m'] / 1000:.1f} km"
f"({shipped_report['nn_tour_virtual_s']:.0f} s 移动)", flush=True)
targets = forward_targets(args.target_step, args.headings)
candidates = candidate_lattice(args.spacing, args.radius)
print(f"候选点 {len(candidates)}({args.spacing} m 网格,半径 {args.radius} m);"
f"设计裕量 r_design={design_radius:.0f} m,前向角裕量 "
f"{args.heading_margin}°", flush=True)
masks = coverage_masks(targets, candidates, r_eff=design_radius,
heading_margin_deg=args.heading_margin)
chosen = greedy_cover(masks, len(targets))
if not args.no_prune:
chosen = prune_redundant(masks, chosen)
best_points = [candidates[i] for i in chosen]
if args.fine_prune:
best_points = fine_prune(best_points, verify_step, verify_headings,
verify_radius, args.verify_heading_margin)
best_report = _report(best_points, "forward_half_disk",
verify_step, verify_headings,
verify_radius, args.verify_heading_margin)
print(f"设计裕量最小命中集:{len(best_points)} 点,缺口 {best_report['uncovered']},"
f"NN 巡回 {best_report['nn_tour_m'] / 1000:.1f} km"
f"({best_report['nn_tour_virtual_s']:.0f} s 移动)", flush=True)
if best_report["uncovered"] != 0:
print("贪心结果未通过证书校核,拒绝生成计划。")
return 1
generator = {"script": "scripts/verify_q4_cover.py",
"spacing_m": args.spacing, "radius_m": args.radius,
"target_step_m": args.target_step, "headings": args.headings,
"design_r_eff_m": design_radius,
"heading_margin_deg": args.heading_margin,
"pruned": not args.no_prune,
"verify_step_m": verify_step,
"verify_headings": verify_headings,
"verify_r_eff_m": verify_radius,
"verify_heading_margin_deg": args.verify_heading_margin,
"code_sha256": hashlib.sha256(
Path(__file__).read_bytes()).hexdigest()}
certificate = CoverageCertificate(
kind=best_report["kind"], target_step_m=verify_step,
headings=verify_headings, r_eff_min_m=verify_radius,
targets=best_report["targets"], uncovered=best_report["uncovered"],
worst_hit_distance_m=best_report["worst_hit_distance_m"],
nn_tour_m=best_report["nn_tour_m"],
generator_sha256=generator["code_sha256"],
verify_heading_margin_deg=args.verify_heading_margin)
plan = CoveragePlan(
name=args.write_plan or "q4_mincover_draft", problem=4,
kind="certified", coverage="forward_half_disk",
note=(f"设计裕量最小命中集:{len(best_points)} 点,"
f"r_design={design_radius:.0f} m / 前向角裕量 {args.heading_margin}°;"
f"证书按 {verify_step:.0f} m/{verify_headings} 档、r_verify="
f"{verify_radius:.0f} m、角裕量 {args.verify_heading_margin}° "
f"校核缺口 0(同一证书口径复核,含精细剪枝);最坏命中 "
f"{best_report['worst_hit_distance_m']:.0f} m,"
f"NN 巡回 {best_report['nn_tour_m'] / 1000:.1f} km"),
generator=generator, certificate=certificate,
points=tuple(best_points))
payload = {"model": {"r_omega_m": 1800.0, "r_eff_min_m": 1000.0},
"design": {"r_design_m": design_radius,
"heading_margin_deg": args.heading_margin},
"verify": {"r_eff_m": verify_radius,
"heading_margin_deg": args.verify_heading_margin,
"step_m": verify_step, "headings": verify_headings},
"shipped": shipped_report, "mincover": best_report}
if args.out is not None:
out = args.out if args.out.is_absolute() else ROOT / args.out
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
print(f"证书已写入 {out}")
if args.write_plan:
target = DEFAULT_PLAN_DIR / f"{args.write_plan}.json"
write_plan(plan, target)
verified = verify_plan(plan)
print(f"计划已写入 {target};运行期分辨率复核缺口 {verified.uncovered}")
print("已注册计划:" + ",".join(plan_names()))
_ = (ProblemConfig(), omega_positions) # 保持公开入口可发现性
return 0
if __name__ == "__main__":
raise SystemExit(main())