Files
iomgaa 852600df02 feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步
消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。
eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 10:36:20 -04:00

20 KiB
Raw Permalink Blame History

填圆任务(n=26,单位正方形)

这份文件写的是题目本身和任务契约:题目是什么、评测怎么判分、产物写在哪、键名怎么写。 研究者读它弄清题目和评测;实验员照它写代码,提案里的内容这里不重复。

目标

在单位正方形 [0,1]² 内放置 26 个互不重叠、不越过边界的圆,最大化半径之和。

最终指标:sum-of-radii,越大越好。

量级参照:容差 0 的口径下,26 个圆公开的最好半径和约 2.635983(AlphaEvolve 续作公开的构造, 按它自己的严格校验在本机重算得 2.6359830849)。常见的"2.635"是白皮书截到三位小数的数,不是另一个纪录。 别处报告的更高的数要先看容差:容差放到 1e-6,把每个半径加 5e-7 也判合格,和就能到 2.635996——那是口径不同,不是更好的摆法。

成绩是从你交出来的产物算的,不是从你返回的那个数读的。 validate.py 读 solution.json 里的 26 个半径,用 math.fsum 求和,那就是成绩。

你在 run_packing() 里返回的第三个值(sum_of_radii)不是成绩。 harness.py 照常把它写进 solution.json 的 reported_sum 字段,但 validate.py 根本不读这个字段:它不进判定、不进诊断,也不跟任何数对账。 所以这个数写过期了、跟半径对不上,产物照样合法,成绩照样是算出来的那一个。

只有一件事要注意,而且跟对账无关:harness.py 落盘时不接受非有限数, 所以这个值要是 NaN 或无穷大,整份 solution.json 根本写不出来, 这次运行连产物都没有。返回一个普通的有限数就行,哪怕它是错的。

评测会收下什么

判断代码里一段检查、修复或兜底"挡掉了谁、改了什么",要先知道评测本身会收下什么。 评测收下一次运行,按下面的次序:

  1. 你交的 solution.json 形状对(26 个圆心、26 个半径)、全是有限数、半径不为负、圆心都在正方形里。 这几条任何一条不成立,评测不收,也不修。
  2. 原产物按容差 0 判:任意两圆不重叠、每个圆不越出正方形边。成立就收下,成绩是原产物的半径和。
  3. 不成立时,先按下一节"只缩不移合法化"修一次,修后再按容差 0 判。修得好就收下,成绩是修后的半径和 (只比原产物低一点);修不好才不收。

所以"差一点不合法"(重叠或越界只有浮点零头那么大)的产物,评测本来会收,只扣掉零头。 候选代码里比这更严的检查——例如容差 0 判不合法就把这个起点整个丢掉、或换成另一份解——挡掉的正是评测会收的产物, 它改的是交出去的答案,不是"让代码合法"。反过来,只在评测确实不收的情形(形状不对、非有限数、圆心出界) 才动手修或换的代码,在评测会收的运行上不改答案。

差一点不合法时:只缩不移合法化

校验器先按容差 0 判你交的原产物。原产物不合法时,宿主按下面这条写死的规则修一次, 只缩半径、不挪圆心,修好了就用修后的半径和当成绩:

  1. **只缩参与违反的圆。**每个圆的半径减去它参与的最大违反量——跟别的圆重叠的量、 越出正方形边的量,取最大的那个。没参与违反的圆一个都不动。
  2. 缩完仍不合法(浮点舍入留下的零头),把这些圆的半径各往 0 挪一个最小的浮点单位, 再判,直到合法。
  3. 同一个校验器按容差 0 重判修后的产物。
  4. 修不了:有圆心在正方形外面;参与违反的圆半径都缩到 0 仍不合法; 形状不对、含非有限值或负半径(这些不是"差一点",缩半径解决不了)。 修不了的运行照旧记失败,运行次数照扣——跟跑崩、超时、没产物一样。

缩小只会降分,所以这条规则不会让不合法的摆法比合法的更高分。它管的是"浮点上差一点": 例如两个圆重叠 2.8e-17,修后扣掉约 2.2e-16。要一分不扣,就在代码里给每个圆留一点空隙。

每次运行多记三个数,跟成绩一起进运行记录:原违反量(raw-violation)、 原分数(raw-score)、合法化扣掉的分(legalization-deduction,原产物合法时是 0)。

什么样的结果才算数

下面五条不是禁令,是这次实验要成立需要的条件。写出来是为了让你知道该往哪边走, 不是为了防谁。

一 · 成绩必须是算出来的,不能是写死的

代码里可以有常数:半径比例、松弛步长、网格怎么划分、迭代多少轮,都可以写死。 不能有的是一张 26 行的答案表——把某次跑出来的圆心和半径直接抄进源码返回。

为什么:研究要找的是方法,不是一个数。一份写死的答案没有机制可以解释—— 问它"为什么这个改法有效",答案是"因为答案是抄的",而这句话不会出现在提案里。 一个没有方法的高分会让整轮研究的结论是假的:后面几轮会建立在 "某某方向有效"这个不存在的发现之上。

二 · 只读工作目录里的东西,不联网

你可以带自己的数据文件,它们会跟代码一起冻结、一起放进工作目录。 但运行时不要去读工作目录外面的文件。正式运行的容器是断网的,网络请求一律失败。

为什么:冻结的代码要能完整说清"这次跑的是什么"。 从网上取一份东西进来,那份东西明天可能就变了,这次运行就再也重现不了; 读工作目录外面的文件也一样,那个文件不在冻结的代码里。

三 · 在执行限额之内跑完

每次正式运行的墙钟时限是 120 秒、1 核,同一批候选都一样。到时限还没结束,进程被直接杀掉, 这次运行记为超时失败,运行次数照扣。

为什么:跟基线比的前提是同样的条件。用一百倍时间换来的成绩, 说明的是算力,不是方法。

四 · 用了多少算力要说出来

每次运行都记墙钟时间(wall-seconds)、CPU 秒(cpu-seconds)和峰值内存(peak-memory-bytes), 材料里同时写"是基线的几倍"和"占单次时限的几成"。这些只记录,不影响采纳。

加大搜索预算本身是正当的做法,不需要藏着。但用远多于基线的算力换来的提升, 说明的是算力,不是方法——所以要在提案里写明加了多少、为什么值得。

为什么:跟基线比的前提是同样的条件。一次多花一百五十倍时间、换来百分之七提升的运行, 如果被记成"某某改法有效",后面几轮会去改那个改法,而真正起作用的是预算。

五 · 说了什么就要真的做什么

代码和注释里写了"迭代到收敛",就要真的迭代到收敛;写了"最多 1000 步", 就不能实际只跑 2 步。

为什么:这一条跟前面几条不一样,它坏的不是成绩,是解释。 成绩可能完全是真的、可复现的,但"松弛到收敛有效"这条结论会进记录, 下一轮建立在一个没有发生过的机制之上。对不上的时候, 该作废的是那条结论,不是这次运行。

这也是为什么第一条说"没有方法的高分会让整轮研究的结论是假的"—— 证据链上的错,后面每一轮都看不见。

运行方法

候选可以是多文件 Python 项目,普通相对数据资源会保留;入口 initial.py 必须定义:

def run_packing():
    ...
    return centers, radii, sum_of_radii  # (26,2)、(26,)、float

正式运行由程序启动,在任务镜像里、断网、1 核、120 秒时限下执行(工作目录就是你的代码目录):

python -B harness.py --program initial.py --output solution.json

跑完以后,宿主把 solution.json 复制到另一个目录,用自己保管的校验器判分:

python -B validate.py --circles 26 --solution solution.json --atol 0 --legalize --verdict verdict.json

harness.py 在 import 候选前,用环境变量 INFORSI_SEED 给的种子字符串(取 SHA256 前 4 字节大端整数) 初始化 Python 的 random 和 numpy 的 random,执行候选并原子落盘原始解,不做任何有效性判断。 import 之前它还会删掉工作目录里已有的 solution.json 和 metrics.json,宿主读到的只会是这一次写的。 你要用随机,用这两个。同一份研究的全部运行用同一个种子,所以同一份代码跑两次,结果逐位相同。 task.json 的 unversionedNames 列的名字(__pycache__、.git、solution.json、metrics.json)在任何一层目录都不会存进代码版本:程序冻结你的工作目录时跳过它们,所以不要把要交的代码或数据放在这些名字下面。

validate.py 是独立校验器,重算半径和并检查形状 / 有限 / 非负 / 边界 / 重叠; 加 --legalize 时按上面"只缩不移合法化"一节修差一点不合法的产物(正式评测就是这样调的)。

写代码的过程中可以做轻量检查:由宿主在同一个镜像里跑,只看语法和入口函数, 不执行候选的任何一行代码。它有次数和时长上限(每次几秒),不算一次正式运行。

不要自己跑正式规模的求解。 正式运行要先登记、再启动、再扣次数,由程序统一做; 正式规模的求解哪怕叫"调试",也是正式运行,不能借轻量检查或自己的 shell 绕开预算。

诊断实验怎么拿到原始产物

诊断实验(在当前基线上只动一处、再看分数)不走上面的 run_packing()。实验员把本轮选中的全部实验写进一份诊断程序, 它从当前基线的代码出发,入口仍是 initial.py,但要定义的是:

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 运行时的 cwd,不要写绝对路径。

metrics-spec.json:宿主写给你的

宿主在准备运行目录时写好它,内容是这次运行登记的假设专用指标定义, 一个 JSON 数组,每个元素是一条指标:

字段 含义
id 指标编号,宿主按它取值
meaning 这个量测的是什么
unit 单位
direction maximize 越大越好,minimize 越小越好
howMeasured 研究者写的测法:在什么时刻测、测几次、怎么合成一个数。你照着它实现
missing 测不到时这次运行怎么算:recordMissing 只记缺失、成绩照常;invalidateRun 整次作废

没有假设专用指标时,这个文件是一个空数组 []。

metrics.json:你写给宿主的

一个 JSON 对象:键必须是 metrics-spec.json 里那条指标的 id 字段原文, 值是有限数或 null。另外每次都要写两个保留键(下面"两个保留键"一节),它们不是指标、值不是数。

编号从 metrics-spec.json 里照抄,不要自己拼。宿主按编号取值,键对不上, 这条指标就记成"测不到"加一句原因——而成绩完全正常,报告上看不出任何异常, 只是研究者分析机制时悄悄少了一块证据。

四条硬要求:

  1. 键照抄 id,见上。
  2. 值是有限数或 null。测不到就写 null,不要写 0。 写 0 之后宿主分不出"测到了一个 0"和"没测到",而这种错事后从证据里查不出来。 算出 NaN / inf(退化布局里零半径参与的比值最常见)也一样,那是"没测到"。 harness 会把非有限值原样改写成 null 并在 stderr 记一行,退出码仍是 0—— 成绩不受影响,但那条指标这一次就是没有数据。
  3. 必须在 run_packing() 返回之前写完。 harness 在候选返回后立刻整理这个文件; 之后再写进去的内容不会被整理(非有限值不会变成 null,裸 NaN 会让宿主整份读不出来, 这一次所有假设专用指标一起丢,不只是坏的那一条)。
  4. 键多了无所谓,宿主只按 metrics-spec.json 里的编号取值;键少了,缺的那条记"测不到"加一句原因。

文件不存在表示这一次的假设专用指标一个都没测到——这是一个明确状态,不是错误, 但它和"写错了键名"在报告里看起来一样,所以别靠它。

测不到的后果看 missing:选了 invalidateRun 的指标测不到,这次运行整次作废、不能被采纳, 运行次数照扣;选了 recordMissing 的只记缺失。

两个保留键:答案出自哪条路径

研究者不能运行代码,"最终交的答案到底是哪段代码给的"只能读代码猜,猜错了没人发现。 所以每次运行由你的代码自己说出来,写在同一个 metrics.json 里:

键 值 写什么
answer-path 字符串 最终交的答案出自哪里:main(正常主流程),或冻结协议因子清单里某个因子的编号(p1、p2……),或保障清单里某条保障的短名
safeguard-triggers 对象 键是冻结协议保障清单里的每一条保障短名,值是这次运行里它动手了几次(非负整数,没动手写 0);保障清单为空时写 {}
  • 这两个键不是指标,不参与成绩判定;写错、没写都不作废运行,宿主照实记"候选没写答案路径" 或"不认识的路径:原文"。但研究者下一阶段就靠它看"这一格量到的是因子还是保障",所以要如实写。
  • 消融格(关掉某个因子的版本)同样要写:关掉的因子不会是答案路径;如果关掉之后答案改由某条保障给出,就写那条保障。
  • harness 不改写这两个键(指标值才会把非有限数改写成 null)。同样要在 run_packing() 返回之前写完。

一小段示例

import json
import math

SPEC_FILE = "metrics-spec.json"
METRICS_FILE = "metrics.json"


def run_packing():
    with open(SPEC_FILE, encoding="utf-8") as handle:
        spec = json.load(handle)
    # spec[i]["id"] 就是宿主认的编号,spec[i]["howMeasured"] 是照着实现的测法。
    # 这里按这次实际条数对号入座,下面只演示一条。
    spread_id = spec[0]["id"]

    centers, radii, reported_sum = solve()

    spread = float(max(radii) - min(radii))
    measured = {
        spread_id: spread if math.isfinite(spread) else None,
        # 两个保留键:答案出自主流程;协议里的一条保障 "shrink-repair" 这次没动手。
        "answer-path": "main",
        "safeguard-triggers": {"shrink-repair": 0},
    }

    # 在 return 之前写完,路径相对工作目录。
    with open(METRICS_FILE, "w", encoding="utf-8") as handle:
        json.dump(measured, handle)

    return centers, radii, reported_sum

最终指标 sum-of-radii 和合法性判定不经过这条链:它们由 validate.py 从 solution.json 独立重算,你不用也不该在 metrics.json 里写它们。

允许修改范围

候选只能修改自己代码目录里的 *.py 和自带的数据文件(基线的入口是 initial.py,其中 EVOLVE-BLOCK-START / EVOLVE-BLOCK-END 之间是官方预留的进化区域)。 不许改的文件见同目录 task.json 的 protectedFiles;程序会检查有没有改它们。 harness.py 和 metrics-spec.json 在准备运行目录时由宿主放进去,你目录里的同名文件会被盖掉; 校验器和轻量检查脚本由宿主保管,不在你的目录里。也不要改校验容差或执行限额。

基线与许可

baseline/ 为 SakanaAI/ShinkaEvolve 官方代码,Apache-2.0(见 baseline/LICENSE), 固定提交与 sha256 见 baseline/SOURCES.md。基线的嵌套缩半径循环与退化布局是 刻意保留的原始事实,不得"修复"——任何改进必须作为正式候选经过研究流程。

依赖

Python 3.13。正式实验环境里装的第三方库只有下面这些,版本与 requirements.txt 逐行一致:

包 版本 用途
numpy 2.3.1 数组与矩阵运算
scipy 1.18.1 数值优化、线性规划、空间距离计算等
numba 0.67.0 把 Python 循环编译成机器码
llvmlite 0.49.0 numba 的编译后端
sympy 1.14.0 符号与精确计算
mpmath 1.3.0 任意精度浮点运算(sympy 依赖它)
networkx 3.7 图结构分析

**依赖只有镜像里的这几个包。**运行时不能再装别的包:正式运行在固定的镜像里跑、容器断网, 你在工作目录里装的包进不了正式运行。

**每次运行只有 1 核。**容器按 --cpus=1 限总量,环境里 PYTHON_CPU_COUNT=1, 所以 os.cpu_count()、multiprocessing.cpu_count() 都报 1。多开进程或线程不会更快, 只会互相抢这 1 个核、少干活。数学库(OMP_NUM_THREADS)和 numba(NUMBA_NUM_THREADS)的线程数都固定为 1。numba 首次编译函数的时间算在这次运行的墙钟里。

**环境由镜像钉死。**任务跑在 Dockerfile 建出来的镜像里,这些库都烧在那个镜像里。 镜像在研究开始时建一次,镜像编号冻结进账本;一份研究不能自己改依赖。

(给维护者:Dockerfile 的基础镜像经 dockerproxy.net 拉取,换机器时可能要改这一行。)