# 填圆任务(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` 必须定义: ```python def run_packing(): ... return centers, radii, sum_of_radii # (26,2)、(26,)、float ``` 正式运行由程序启动,在任务镜像里、断网、1 核、120 秒时限下执行(工作目录就是你的代码目录): ```bash python -B harness.py --program initial.py --output solution.json ``` 跑完以后,宿主把 `solution.json` 复制到另一个目录,用自己保管的校验器判分: ```bash 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`,但要定义的是: ```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` 运行时的 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()` 返回之前写完。 ### 一小段示例 ```python 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` 拉取,换机器时可能要改这一行。)