feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步

消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。
eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 10:36:20 -04:00
parent f39f8410e7
commit 852600df02
69 changed files with 6607 additions and 214 deletions
+28
View File
@@ -154,6 +154,34 @@ import 之前它还会删掉工作目录里已有的 `solution.json` 和 `metric
**不要自己跑正式规模的求解。** 正式运行要先登记、再启动、再扣次数,由程序统一做;
正式规模的求解哪怕叫"调试",也是正式运行,不能借轻量检查或自己的 shell 绕开预算。
## 诊断实验怎么拿到原始产物
诊断实验(在当前基线上只动一处、再看分数)不走上面的 `run_packing()`。实验员把本轮选中的全部实验写进**一份诊断程序**,
它从当前基线的代码出发,入口仍是 `initial.py`,但要定义的是:
```python
def run_experiment(experiment_id, baseline_dir):
...
return centers, radii, sum_of_radii # 与 run_packing() 的返回值同形
```
- `experiment_id` 是实验编号原文,函数里按它分派到各个实验;每个实验只动它写的那一处,其余照基线。
- `baseline_dir` 是**产出当前基线的那次正式运行的原始产物**所在的目录,相对工作目录,现在就是 `baseline-artifact`。
里面有一个文件 `solution.json`:当时那次运行交出的原始解(`centers`、`radii`、`reported_sum`,合法化之前),
实验要接着算的答案就在里面。程序把它**只读**地放进你的工作目录(文件权限 0444),运行时每个实验读到的是同一份;
不要改它,也不要把它当成自己代码的一部分:存代码版本时 `baseline-artifact` 这个名字被跳过。
- 一次诊断运行是**一个容器、一次正式运行的名额**,总时限和核数同上(120 秒、1 核)。容器里的诊断 harness
(`harness-diagnosis.py`,宿主保管,由程序放进运行目录)按实验顺序一个一个跑,每个实验开一个单独的进程,
分到**总时限 ÷ 实验数**的时间,到时被直接杀掉。numba 的首次编译、数据载入都算在这份时间里。
一个实验崩了或超时,只记这一个实验失败,后面的照常跑;整次运行被总时限杀掉时,没轮到的实验记"没轮到"。
- harness 记下每个实验实际用了多久、有没有超时。每个实验的答案写在 `answers/<实验编号>/solution.json`,
由宿主的校验器按上面"评测会收下什么"的次序**逐个单独判分**(包括只缩不移合法化)。
- 实验登记的假设专用指标,写法同下面"指标怎么交给宿主":在 `run_experiment` 返回之前往工作目录的 `metrics.json` 写,
键照抄 `metrics-spec.json` 里**这个实验自己登记**的指标编号(该文件里是本轮全部实验登记的指标,别的实验的不用写)。
每个实验开始前 harness 会清掉工作目录里的 `metrics.json`,不会串到下一个实验。诊断实验登记的指标一律是"缺了只记缺失",
`answer-path` 和 `safeguard-triggers` 两个保留键不用写。
- 诊断实验的答案**不会被采纳为基线**,只用来看这个方向还剩多少空间。
## 指标怎么交给宿主(这一节最容易写错,先读完再动手)
工作目录里有两个指标文件,一进一出。**路径都相对工作目录**,也就是 `harness.py`
+125
View File
@@ -0,0 +1,125 @@
"""填圆任务的诊断运行 harness(宿主的固定工具,候选不得修改)。
一次诊断运行是一个容器、一次正式运行的名额,里面按顺序跑若干个诊断实验(docs/方法.md 4.3"诊断"一段)。
每个实验各开一个子进程:实验崩了、超时了,只影响它自己,后面的实验照常跑。
**它不判定解是否合法**:每个实验交出的答案,事后由独立校验器 ``validate.py`` 逐个重算。
读候选程序、写原始解、整理 metrics.json 都用 ``harness.py`` 里现成的函数,两份 harness 在这些事上不分叉。
命令行(由宿主的运行定义给出,实验员不用自己跑):
python -B harness-diagnosis.py --program <程序> --baseline <目录> --answers <目录>
--seconds-each <秒> --report <文件> --experiments <实验编号> [<实验编号> ...]
每个实验:
- 调用候选程序里的 ``run_experiment(experiment_id, baseline_dir)``,返回 (centers, radii, reported_sum),
与 ``run_packing()`` 的返回值同形。``experiment_id`` 是实验编号原文,``baseline_dir`` 是只读的原始产物目录
(相对工作目录),里面是产出当前基线的那次正式运行的 ``solution.json``。
- 分到的时限是 ``--seconds-each``(总时限 ÷ 实验数),到时被直接杀掉。
- 随机种子同正式运行:环境变量 INFORSI_SEED,每个实验的子进程各自从头设一次。
- 答案写进 ``<answers>/<实验编号>/solution.json``。指标写法同 TASK.md"指标怎么交给宿主":
在 ``run_experiment`` 返回之前往工作目录里的 ``metrics.json`` 写,写完后 harness 把它整理好、
搬到 ``<answers>/<实验编号>/metrics.json``,下一个实验开始前工作目录里不留它。
每个实验结束后,harness 重写一次 ``--report``:每个实验一项
``{"id", "status", "wall_seconds"}``,status 是 finished(子进程退出码 0)、crashed(非零退出)、
timeout(超过分到的时限,被杀)。跑到哪写到哪:整个容器被总时限杀掉时,已经跑完的实验还留着记录,
没跑到的实验没有记录。
"""
import argparse
import hashlib
import importlib
import json
import os
import random
import signal
import subprocess
import sys
import time
from typing import Any, cast
import harness
SOLUTION_FILE = "solution.json"
def run_experiment_child(args) -> int:
"""子进程:设种子、载入候选程序、跑一个实验、落盘答案和指标。"""
np = importlib.import_module("numpy")
seed = int.from_bytes(
hashlib.sha256(os.environ["INFORSI_SEED"].encode()).digest()[:4], "big"
)
random.seed(seed)
np.random.seed(seed)
module = harness.load_program(args.program)
run_experiment = getattr(module, "run_experiment", None)
if not callable(run_experiment):
raise RuntimeError(f"{args.program} 没有定义可调用的 run_experiment(experiment_id, baseline_dir)")
centers, radii, reported_sum = cast(Any, run_experiment(args.id, args.baseline))
solution = {
"centers": [[float(x), float(y)] for x, y in centers],
"radii": [float(r) for r in radii],
"reported_sum": float(reported_sum),
}
harness.write_json_atomically(args.output, solution)
harness.finalize_metrics()
return 0
def run_experiment_with_limit(args, experiment_id: str) -> tuple[str, float]:
"""在子进程里跑一个实验,交回结局和用时。"""
answer_dir = os.path.join(args.answers, experiment_id)
os.makedirs(answer_dir)
# 上一个实验或候选目录里遗留的指标文件,不能被当成这个实验的。
if os.path.exists(harness.METRICS_FILE):
os.remove(harness.METRICS_FILE)
command = [
sys.executable, "-B", os.path.abspath(__file__), "--child",
"--program", args.program, "--baseline", args.baseline,
"--id", experiment_id, "--output", os.path.join(answer_dir, SOLUTION_FILE),
]
start = time.monotonic()
# 新开一个进程组:超时时连实验自己开的子进程一起杀掉。
child = subprocess.Popen(command, start_new_session=True)
try:
status = "finished" if child.wait(timeout=args.seconds_each) == 0 else "crashed"
except subprocess.TimeoutExpired:
os.killpg(child.pid, signal.SIGKILL)
child.wait()
status = "timeout"
wall = time.monotonic() - start
if os.path.exists(harness.METRICS_FILE):
if status == "finished":
os.replace(harness.METRICS_FILE, os.path.join(answer_dir, harness.METRICS_FILE))
else:
os.remove(harness.METRICS_FILE)
return status, wall
def main() -> int:
parser = argparse.ArgumentParser(description="按顺序运行若干个诊断实验,各自限时、互不影响。")
parser.add_argument("--program", required=True)
parser.add_argument("--baseline", required=True)
parser.add_argument("--child", action="store_true")
parser.add_argument("--id")
parser.add_argument("--output")
parser.add_argument("--answers")
parser.add_argument("--seconds-each", type=float)
parser.add_argument("--report")
parser.add_argument("--experiments", nargs="+")
args = parser.parse_args()
if args.child:
return run_experiment_child(args)
report: list[dict] = []
for experiment_id in args.experiments:
status, wall = run_experiment_with_limit(args, experiment_id)
print(f"[诊断实验 {experiment_id}] {status},用时 {wall:.2f} 秒", file=sys.stderr, flush=True)
report.append({"id": experiment_id, "status": status, "wall_seconds": wall})
harness.write_json_atomically(args.report, report)
return 0
if __name__ == "__main__":
sys.exit(main())