From 4b1c0e74f308b3bd7f353460fb8621c35e2b72e9 Mon Sep 17 00:00:00 2001
From: zzy <17092805+mz2007@user.noreply.gitee.com>
Date: Sat, 5 Sep 2026 16:16:22 +0800
Subject: [PATCH 1/6] feat: add ContinuousCasting domain (CuttingOptimization +
CuttingOptimizationOnline)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two original benchmarks inspired by CUMCM 2021 Problem D, formalized into deterministic,
stdlib-only, verifiable optimization tasks.
- CuttingOptimization (offline): cut a continuously cast billet (with fixed 0.8 m scrap
segments) into pieces to minimize total scrapped length, then match a customer target.
Reference DP = 88.7, baseline = 72.5; the optimum is reachable (openevolve hits 88.7).
- CuttingOptimizationOnline (closed-loop): scrap segments are revealed only within a
reveal_lead horizon; the agent decides each cut with only the visible past and is scored
against the full hidden defect set. Clairvoyant reference = 76.4, baseline = 52.4,
agents (3 frameworks x 3 runs) = 70.43 +- 0.83 — an online agent cannot reach the
clairvoyant optimum (info asymmetry), which is the key difference vs offline.
Integrity/anti-cheat mirrors the accepted CVRP/TelecomBackup benchmarks: reference solver and
generator excluded from the sandbox + forbidden by the validator, FRONTIER_* env stripping
(host side-channel), runtime instance generation, EVOLVE-BLOCK fixed-region check, determinism
probe, sandbox-evaluator test coverage, multiseed_stat tool, and honest README/Task docs.
Unit tests: 35 (offline) + 23 (online) pass.
---
TASK_DETAILS.md | 9 +
TASK_DETAILS_zh-CN.md | 9 +
.../CuttingOptimization/.gitignore | 2 +
.../CuttingOptimization/README.md | 136 +++++++++
.../CuttingOptimization/README_zh-CN.md | 94 ++++++
.../CuttingOptimization/Task.md | 105 +++++++
.../baseline/result_log.txt | 31 ++
.../CuttingOptimization/baseline/solver.py | 76 +++++
.../frontier_eval/agent_files.txt | 4 +
.../frontier_eval/artifact_files.txt | 2 +
.../frontier_eval/constraints.txt | 19 ++
.../frontier_eval/copy_files.txt | 6 +
.../frontier_eval/eval_command.txt | 1 +
.../frontier_eval/eval_cwd.txt | 1 +
.../frontier_eval/evaluator.py | 36 +++
.../frontier_eval/initial_program.txt | 1 +
.../frontier_eval/readonly_files.txt | 4 +
.../frontier_eval/run_eval.py | 126 ++++++++
.../data/instances/instance_1.json | 30 ++
.../data/instances/instance_2.json | 30 ++
.../data/instances/instance_3.json | 38 +++
.../data/instances/instance_4.json | 38 +++
.../data/instances/instance_5.json | 30 ++
.../data/instances/instance_6.json | 50 ++++
.../data/instances/instance_7.json | 42 +++
.../data/instances/instance_8.json | 42 +++
.../verification/docker/Dockerfile | 13 +
.../verification/evaluate.py | 276 ++++++++++++++++++
.../verification/generator.py | 202 +++++++++++++
.../verification/multiseed_stat.py | 87 ++++++
.../verification/ref_solver.py | 111 +++++++
.../verification/requirements.txt | 2 +
.../verification/simulator.py | 169 +++++++++++
.../verification/test_evaluator.py | 61 ++++
.../test_frontier_eval_evaluator.py | 103 +++++++
.../verification/test_generator.py | 57 ++++
.../verification/test_ref_solver.py | 50 ++++
.../verification/test_simulator.py | 124 ++++++++
.../verification/test_validator.py | 80 +++++
.../verification/validator.py | 154 ++++++++++
.../CuttingOptimizationOnline/.gitignore | 2 +
.../CuttingOptimizationOnline/README.md | 151 ++++++++++
.../CuttingOptimizationOnline/README_zh-CN.md | 116 ++++++++
.../CuttingOptimizationOnline/Task.md | 80 +++++
.../baseline/result_log.txt | 28 ++
.../baseline/solver.py | 56 ++++
.../frontier_eval/agent_files.txt | 4 +
.../frontier_eval/artifact_files.txt | 2 +
.../frontier_eval/constraints.txt | 22 ++
.../frontier_eval/copy_files.txt | 6 +
.../frontier_eval/eval_command.txt | 1 +
.../frontier_eval/eval_cwd.txt | 1 +
.../frontier_eval/evaluator.py | 31 ++
.../frontier_eval/initial_program.txt | 1 +
.../frontier_eval/readonly_files.txt | 4 +
.../frontier_eval/run_eval.py | 126 ++++++++
.../data/instances/instance_1.json | 33 +++
.../data/instances/instance_2.json | 33 +++
.../data/instances/instance_3.json | 41 +++
.../data/instances/instance_4.json | 53 ++++
.../data/instances/instance_5.json | 45 +++
.../data/instances/instance_6.json | 77 +++++
.../data/instances/instance_7.json | 69 +++++
.../data/instances/instance_8.json | 77 +++++
.../verification/docker/Dockerfile | 13 +
.../verification/evaluate.py | 223 ++++++++++++++
.../verification/generator.py | 100 +++++++
.../verification/multiseed_stat.py | 87 ++++++
.../verification/ref_solver.py | 109 +++++++
.../verification/requirements.txt | 2 +
.../verification/simulator.py | 154 ++++++++++
.../verification/test_evaluator.py | 63 ++++
.../test_frontier_eval_evaluator.py | 65 +++++
.../verification/test_generator.py | 41 +++
.../verification/test_ref_solver.py | 42 +++
.../verification/test_simulator.py | 67 +++++
.../verification/test_validator.py | 73 +++++
.../verification/validator.py | 93 ++++++
benchmarks/ContinuousCasting/README.md | 23 ++
benchmarks/ContinuousCasting/README_zh-CN.md | 19 ++
80 files changed, 4684 insertions(+)
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/.gitignore
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/README.md
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/Task.md
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/baseline/result_log.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/baseline/solver.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/agent_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/artifact_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/constraints.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/copy_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_command.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_cwd.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/evaluator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/initial_program.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/readonly_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/run_eval.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_1.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_2.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_3.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_4.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_5.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_6.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_7.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_8.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/docker/Dockerfile
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/evaluate.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/generator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/multiseed_stat.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/ref_solver.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/requirements.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/simulator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/test_evaluator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/test_frontier_eval_evaluator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/test_generator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/test_ref_solver.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/test_simulator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/test_validator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimization/verification/validator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/.gitignore
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/result_log.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/solver.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/agent_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/artifact_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/constraints.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_command.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_cwd.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/evaluator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/initial_program.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/readonly_files.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/run_eval.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_1.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_2.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_3.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_4.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_5.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_6.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_7.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_8.json
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/docker/Dockerfile
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/ref_solver.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/requirements.txt
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_evaluator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_frontier_eval_evaluator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_generator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_ref_solver.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_simulator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_validator.py
create mode 100644 benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/validator.py
create mode 100644 benchmarks/ContinuousCasting/README.md
create mode 100644 benchmarks/ContinuousCasting/README_zh-CN.md
diff --git a/TASK_DETAILS.md b/TASK_DETAILS.md
index 7475c4cc..c2d2c98b 100644
--- a/TASK_DETAILS.md
+++ b/TASK_DETAILS.md
@@ -346,6 +346,15 @@ We welcome new engineering problem ideas — even without complete verification
EV2GymSmartCharging |
Upstream-aligned EV smart charging scheduling |
+
+ | ContinuousCasting |
+ CuttingOptimization |
+ Cut a continuously cast billet into pieces to minimize scrapped length and match a customer target (offline; the optimum is reachable) |
+
+
+ CuttingOptimizationOnline |
+ Online (closed-loop) cutting where 0.8 m scrap segments are revealed only within a reveal horizon — info asymmetry keeps agents below the clairvoyant optimum |
+
| AdditiveManufacturing |
DiffSimThermalControl |
diff --git a/TASK_DETAILS_zh-CN.md b/TASK_DETAILS_zh-CN.md
index e2a070a2..5e9f0559 100644
--- a/TASK_DETAILS_zh-CN.md
+++ b/TASK_DETAILS_zh-CN.md
@@ -346,6 +346,15 @@ Frontier-Eng 目前已覆盖以下领域的任务。每个任务均配有可运
EV2GymSmartCharging |
上游对齐的电动车智能充电调度 |
+
+ | ContinuousCasting |
+ CuttingOptimization |
+ 把连续浇铸的钢坯切成成品,最小化报废并贴近客户目标值(离线;最优可达) |
+
+
+ CuttingOptimizationOnline |
+ 在线(闭环)切割:0.8m 报废段只在揭示提前量内才告知 agent——信息不对称让 agent 达不到全知最优 |
+
| AdditiveManufacturing |
DiffSimThermalControl |
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/.gitignore b/benchmarks/ContinuousCasting/CuttingOptimization/.gitignore
new file mode 100644
index 00000000..7a60b85e
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+*.pyc
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/README.md b/benchmarks/ContinuousCasting/CuttingOptimization/README.md
new file mode 100644
index 00000000..84d631f1
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/README.md
@@ -0,0 +1,136 @@
+# CuttingOptimization: Online Optimization of Continuous-Casting Cutting (Frontier-Eng Benchmark)
+
+An **original** Frontier-Engineering benchmark inspired by the CUMCM 2021 Problem D
+(«连铸切割的在线优化»), formalized into a self-contained, deterministic optimization task.
+
+A continuously cast steel billet is drawn at a fixed speed. Defects (crystalline-moulder
+anomalies) create 0.8 m scrap segments inside the billet that must be cut out and scrapped.
+A solver receives the billet length, the defect positions, and a customer target length with
+an acceptance window, and must produce a **cutting plan** (a list of cut lengths that exactly
+partition the whole billet) that minimizes the total scrapped length and then makes every
+shipped piece as close to the target length as possible.
+
+The full game rules and evaluation semantics are in [Task.md](./Task.md) (Chinese).
+
+## Layout
+
+```
+benchmarks/ContinuousCasting/CuttingOptimization/
+├── baseline/solver.py # Candidate solver (EVOLVE-BLOCK region is the only editable part)
+├── verification/
+│ ├── generator.py # Fixed-seed instance generator (defects + target window)
+│ ├── simulator.py # Scoring simulator (validate + scrap / penalty metric)
+│ ├── evaluate.py # Evaluation entry (subprocess + time budget + scoring)
+│ ├── validator.py # Integrity checks (static + env stripping + determinism)
+│ ├── ref_solver.py # Reference DP (1-D partition; documented "best" score)
+│ ├── test_simulator.py # Unit tests: simulator correctness
+│ ├── test_generator.py # Unit tests: deterministic / defect feasibility / headroom
+│ ├── test_ref_solver.py # Unit tests: reference-solver validity + optimality
+│ ├── test_validator.py # Unit tests: integrity checks / env stripping / determinism
+│ ├── test_evaluator.py # Unit tests: end-to-end evaluation behavior
+│ ├── data/instances/ # 8 fixed instances (seed-fixed, reproducible)
+│ ├── docker/Dockerfile # Minimal stdlib-only python image
+│ └── requirements.txt
+├── frontier_eval/ # UnifiedTask metadata
+├── Task.md # Task rules, interface, scoring, reference scores
+└── README.md
+```
+
+## Requirements
+
+- Python >= 3.10, standard library only (no third-party dependencies).
+- Runtime is pure-Python; the reference DP and the baseline solver evaluate in well under a
+ second per instance.
+
+## Run
+
+```powershell
+# Score a solver on the fixed 8-instance set (default 60s time budget per instance)
+python verification/evaluate.py baseline/solver.py
+
+# Add runtime-generated instances (anti-hardcoding)
+python verification/evaluate.py baseline/solver.py --generate-seed
+
+# Tighter budget (challenge tier: 10s)
+python verification/evaluate.py baseline/solver.py --time-budget 10
+```
+
+### Docker
+
+The evaluator is pure stdlib, so a minimal `python` image suffices. Build it and use the
+unified runtime's `isolation_mode=docker`:
+
+```bash
+# Build (inside the CuttingOptimization directory)
+docker build -t cutting-opt-benchmark -f verification/docker/Dockerfile .
+```
+
+## Tests
+
+```powershell
+# From the task directory (stdlib unittest, no dependencies)
+python -m unittest discover -s verification -p "test_*.py"
+```
+
+35 tests across six modules (simulator / generator / ref_solver / validator / evaluator /
+sandbox evaluator):
+piece-level scrap & penalty rules, feasibility checks (sum, length window, defect isolation),
+determinism of generation and reference solver, reference-solver optimality vs the baseline,
+validator integrity (EVOLVE-BLOCK / forbidden references / absolute paths / per-instance
+hardcoding / env stripping / determinism probe), evaluator behavior (scoring, runtime
+generation, cheating-candidate rejection), and the `frontier_eval/evaluator.py` sandbox entry
+(consistency with `verification/evaluate.py` + cheat rejection). `verification/multiseed_stat.py`
+computes multi-run mean ± std.
+
+## Integrity / threat model
+
+- **Runtime-generated instances**: with `CUTTING_EVAL_GENERATE_SEED` set, the evaluator
+ generates fresh instances at evaluation time (temp dir, never in the repo/sandbox), so a
+ candidate cannot pre-position solutions for them.
+- **Candidate env stripping**: candidate subprocesses get `FRONTIER_*` / `CUTTING_EVAL_*`
+ variables stripped (see `verification/validator.py`), closing the host-env side channel.
+- **Static checks**: EVOLVE-BLOCK markers + fixed-region byte diff vs the initial baseline,
+ forbidden imports of evaluation / generation / reference modules, absolute paths,
+ per-instance hardcoding, plus a determinism probe (two runs must match). Any violation
+ scores 0.
+- **Sandbox scope**: the 8 fixed instances and the evaluator / validator sources are visible
+ to the candidate during evolution (they are needed for scoring and `verification/simulator.py`
+ is intentionally usable as a white-box scorer). Anti-hardcoding therefore relies on
+ `CUTTING_EVAL_GENERATE_SEED` (fresh instances at evaluation time — set a seed, do not use a
+ fixed one); the name-keyed hardcoding check is best-effort. `verification/ref_solver.py` and
+ `verification/generator.py` are **not** copied into the sandbox and are additionally forbidden
+ by the validator.
+- Honest note: in process mode the candidate has host filesystem access (framework-wide
+ limitation); this benchmark relies on the layered defenses above.
+
+## Scoring
+
+- Instances = 8 fixed (difficulties easy/medium/hard, S = 24..150 m, 0..6 defects, target
+ window ±0.5 m around the customer target) + runtime-generated when `CUTTING_EVAL_GENERATE_SEED`
+ is set.
+- **Metric**: material utilization = `100 * (billet_length - (scrap + 1e-4*penalty)) / billet_length`,
+ averaged over instances (0..100, higher is better). `scrap` = total scrapped length (defect
+ pieces + sub-8.0 m pieces + over-window excess); `penalty = Σ|delivered − target|` over shipped
+ pieces; the `1e-4` weight is so small that scrap strictly dominates, respecting the lexicographic
+ objective of the original problem (the penalty only breaks ties among equal-scrap plans).
+- Malformed output / out-of-range cuts / cuts that fail to isolate a defect / crash / timeout
+ ⇒ 0 points for that instance.
+- **Headroom guarantee**: the generator accepts only instances where the reference DP strictly
+ beats a naive equal-split baseline by ≥ 0.1 m of scrap, so every instance has real
+ optimization signal.
+- Reference scores (verified on the fixed 8 instances, `verification/evaluate.py`):
+ - baseline (equal-split, no target-awareness): **72.5** utilization (mean scrap 25.2 m)
+ - reference DP (`verification/ref_solver.py`, 1-D partition): **88.7** utilization (mean scrap 10.6 m)
+ - per-instance reference utilization: 95.8 / 88.8 / 91.8 / 75.2 / 92.4 / 87.0 / 89.4 / 89.3
+ - agent (openevolve, 10 generations, best saved program): **88.7** utilization
+ (run `20260903_130517`) — **equals the reference DP exactly**.
+ - agent (ShinkaEvolve, 15 generations, via reasoning proxy): **88.4** utilization
+ (near the optimum, run `20260904_182017`).
+ - agent (AB-MCTS, 15 iterations): **72.5** utilization (= baseline; this run did not improve —
+ AB-MCTS is weaker here, and low-reasoning mutations mostly regressed/invalidated).
+ - honest design note: the offline optimum is **reachable** (a strong agent can derive the
+ 1-D partition DP and hit ~88.7 = the ceiling). So the offline task's difficulty is *deriving*
+ the DP, not long-horizon search. The harder **online** companion
+ ([CuttingOptimizationOnline](../CuttingOptimizationOnline/README.md)) is where info
+ asymmetry keeps agents below the clairvoyant ceiling — see that README for the 3×2 matrix.
+ - `verification/multiseed_stat.py` gathers multi-run mean ± std across framework run dirs.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md b/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
new file mode 100644
index 00000000..1ae3af2b
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
@@ -0,0 +1,94 @@
+# CuttingOptimization:连铸切割的在线优化(Frontier-Eng 基准)
+
+一个**原创**的 Frontier-Engineering 基准,灵感来自 2021 全国大学生数学建模竞赛 D 题
+《连铸切割的在线优化》,并被形式化成**确定性、自包含**的切割优化任务。
+
+一根连续浇铸的钢坯以固定速度被拉出;结晶器异常会在坯内产生 **0.8 m 的零废段**,必须被
+切出并报废。求解器收到钢坯长度、零废段位置、以及一个"目标值 + 可接受窗口"的用户需求后,
+要给出一个**切割方案**(一组恰好铺满整根钢坯的切段长度),使**报废总长度最小**,并让
+**每块成品尽量贴近目标值**。
+
+完整规则与评测语义见 [Task.md](./Task.md)。
+
+## 目录结构
+
+```
+benchmarks/ContinuousCasting/CuttingOptimization/
+├── baseline/solver.py # 候选求解器(仅 EVOLVE-BLOCK 区域可改)
+├── verification/
+│ ├── generator.py # 固定种子实例生成器(零废段 + 目标窗口)
+│ ├── simulator.py # 计分模拟器(校验 + 报废 / 贴合度指标)
+│ ├── evaluate.py # 评测入口(subprocess + 时间预算 + 打分)
+│ ├── validator.py # 完整性校验(静态 + 环境剥离 + 确定性)
+│ ├── ref_solver.py # 参考解(一维划分 DP;文档化的"最优"分)
+│ ├── test_simulator.py # 单测:计分器正确性
+│ ├── test_generator.py # 单测:确定性 / 零废段可行性 / headroom
+│ ├── test_ref_solver.py # 单测:参考解合法性 + 最优性
+│ ├── test_validator.py # 单测:完整性校验 / 环境剥离 / 确定性
+│ ├── test_evaluator.py # 单测:端到端评测行为
+│ ├── data/instances/ # 8 个固定实例(种子固定、可复现)
+│ ├── docker/Dockerfile # 极简纯标准库 python 镜像
+│ └── requirements.txt
+├── frontier_eval/ # UnifiedTask 元数据
+├── Task.md # 任务规则、接口、评分、参考分
+└── README_zh-CN.md
+```
+
+## 运行
+
+```powershell
+# 在固定 8 实例上给求解器打分(默认每实例 60s)
+python verification/evaluate.py baseline/solver.py
+
+# 加运行时生成实例(防硬编码)
+python verification/evaluate.py baseline/solver.py --generate-seed
+
+# 更紧的时间预算(挑战档 10s)
+python verification/evaluate.py baseline/solver.py --time-budget 10
+```
+
+## 测试
+
+```powershell
+python -m unittest discover -s verification -p "test_*.py"
+```
+
+共 35 个单测(simulator / generator / ref_solver / validator / evaluator / 沙箱 evaluator 六个模块):
+单块报废与贴合度规则、合法校验(求和、长度窗口、零废段对齐)、生成与参考解的确定性、
+参考解优于 baseline 的最优性、validator 完整性(EVOLVE-BLOCK / 禁引用 / 绝对路径 /
+按实例名硬编码 / 环境剥离 / 确定性探针)、以及 evaluator 行为(打分、运行时生成、
+作弊候选被拒)、`frontier_eval/evaluator.py` 沙箱入口一致性。`verification/multiseed_stat.py`
+用于多轮均值±std。
+
+## 完整性 / 威胁模型
+
+- **运行时生成实例**:设置 `CUTTING_EVAL_GENERATE_SEED` 后,评测现场生成新实例(临时目录,
+ 不进仓库/沙箱),候选无法预先记忆。
+- **候选环境剥离**:候选子进程剥离 `FRONTIER_*` / `CUTTING_EVAL_*` 变量(见 validator.py)。
+- **静态检查**:EVOLVE-BLOCK 标记 + 固定区字节比对、禁引用评测/生成/参考解模块、绝对路径、
+ 按实例名硬编码、确定性探针(两次运行输出一致)。任何违规记 0 分。
+- **沙箱范围**:8 个固定实例、evaluator/validator 源码对候选可见(打分需要,且
+ `verification/simulator.py` 有意作为白盒计分器);防硬编码依赖 `CUTTING_EVAL_GENERATE_SEED`。
+ `verification/ref_solver.py` 与 `verification/generator.py` **不**复制进沙箱,并被 validator 额外禁用。
+- 说明:在进程模式下候选有主机文件系统访问(框架级限制),本基准依赖上述分层防御。
+
+## 评分
+
+- 实例 = 8 固定(easy/medium/hard,S=24..150 m,0..6 个零废段,目标窗口 ±0.5 m)+ 设置
+ `CUTTING_EVAL_GENERATE_SEED` 时的运行时生成实例。
+- **指标**:材料利用率 `util = 100 * (S - (scrap + 1e-4*penalty)) / S`,多实例取平均(0~100,越高越好)。
+ `scrap` = 总报废长度(零废段小块 + <8.0 m 整损 + 超出窗口的余量);`penalty = Σ|交付 - 目标|`;
+ `1e-4` 权重极小,保证报废严格占主导(惩罚仅在报废相同时破平),符合赛题的字典序目标。
+- 非法输出 / 越界切段 / 未对齐零废段 / 崩溃 / 超时 ⇒ 该实例 0 分。
+- **headroom 保证**:生成器只接受"参考解严格优于朴素等分 ≥ 0.1 m 报废"的实例,
+ 保证每个实例都有真实优化信号。
+- 参考分(固定 8 实例实测):
+ - baseline(均匀等分):**72.5** 利用率(平均报废 25.2 m)
+ - ref_solver(一维划分 DP):**88.7** 利用率(平均报废 10.6 m)
+ - agent(openevolve,10 代,best 保存程序):**88.7** 利用率(run `20260903_130517`)——**恰好等于参考解 DP**。
+ - agent(ShinkaEvolve,15 代,经推理代理):**88.4** 利用率(run `20260904_182017`,接近最优)。
+ - agent(AB-MCTS,15 迭代):**72.5** 利用率(= baseline;本轮未提升——AB-MCTS 在此较弱,低推理下多数改进突变回归/无效)。
+ - 诚实设计说明:离线最优是**可达的**(强 agent 能推导出精确的一维划分 DP 并打到 88.7 = 天花板)。
+ 所以离线任务的难度在"推导 DP",不在长程搜索。更难的**在线版**
+ (`CuttingOptimizationOnline`)才是信息不对称让 agent 无法达到全知天花板——见该 README 的 3×2 矩阵。
+ - `verification/multiseed_stat.py` 用于跨框架运行目录做多轮均值±std。
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/Task.md b/benchmarks/ContinuousCasting/CuttingOptimization/Task.md
new file mode 100644
index 00000000..7192e868
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/Task.md
@@ -0,0 +1,105 @@
+# 连铸切割的在线优化(CuttingOptimization)
+
+## 1. 背景
+
+连铸是把钢水变成钢坯的生产过程:钢水从中间包连续浇入结晶器,按固定拉坯速度往下拉,
+经二冷段凝固成钢坯,再按尺寸要求切割。切割机有一个固定的工作起点,切割必须从该起点开始。
+
+当结晶器出现异常时,钢坯内会形成一小段"报废段"(本任务中统一为 **0.8 m**),必须被切出
+并报废。切割方案要同时满足两条要求(字典序):
+
+1. **优先最小化切割损失**:切割损失 = 报废钢坯的总长度。
+2. **其次满足用户需求**:在损失相同的方案中,切出的成品尽量贴近用户目标值。
+
+本任务把赛题抽象成一个**确定性的一维切割优化**问题:钢坯是一根一维线段,上面带若干
+零废段;求解器要给出一个把整根钢坯(含必须切出的零废段)完全切成段的切割方案,使报废
+长度最小、成品贴合目标值。
+
+## 2. 输入(实例)
+
+实例是一个 JSON 文件,由 `verification/generator.py` 按种子生成(评测时实时生成,不可预记忆):
+
+```json
+{
+ "seed": 3,
+ "process": {"speed": 1.0, "cut_time": 3, "return_time": 1, "buffer_len": 60, "scrap_len": 0.8},
+ "billet": {"total_length": 106.2},
+ "customer": {"target": 8.5, "target_min": 8.0, "target_max": 9.0},
+ "defects": [[21.3, 22.1], [42.5, 43.3], [60.1, 60.9]],
+ "limits": {"min_basic": 4.8, "max_basic": 12.6, "min_process": 8.0, "max_process": 11.6}
+}
+```
+
+含义:
+- `billet.total_length`:钢坯总长 S,线段 `[0, S]`。
+- `customer.target` / `target_min` / `target_max`:用户目标值及可接受的切段长度窗口。
+- `defects`:零废段区间 `[a, b)`(每段长 0.8 m)。它们把 `[0, S]` 分成若干"干净坯段"。
+- `limits`:三段长度窗口
+ - `[min_basic, max_basic] = [4.8, 12.6]`:能运走的最小/最大长度(硬约束)。
+ - `[min_process, max_process] = [8.0, 11.6]`:下道工序可直接接受的长度。
+ - 用户窗口 `[target_min, target_max]`:零报废的理想段长范围。
+
+时间参数(拉坯速度 1.0 m/min、切一块 3 min、回程 1 min、结晶器到切割机 60 m)在
+`process` 里给出,仅作为物理背景。由于两次切割最小间隔对应材料长度
+`1.0×(3+1)=4 m < 4.8 m`,**切割机总能跟上,时间不构成约束**,故不参与评分。
+
+## 3. 输出(求解格式)
+
+求解器以 `python baseline/solver.py ` 运行,向 stdout 打印一个 JSON:
+
+```json
+{"cuts": [9.5, 9.4, 8.8, 0.8, 10.0, ...]}
+```
+
+`cuts` 是切段长度列表,依次排开**恰好铺满整个 `[0, S]`**(含每个零废段这个 0.8 m 的小块)。
+即 `sum(cuts) == total_length`(容差 1e-3)。
+
+## 4. 合法性校验(硬约束)
+
+1. `sum(cuts) ≈ S`(容差 1e-3)。
+2. 每块长度必须在 `[4.8, 12.6]`,**除非**它恰好是某个零废段(0.8 m 小块,允许出现)。
+3. 切口必须对齐每个零废段的两个端点:零废段必须被单独切出(其两端必须是切点);
+ 任何跨过零废段的成品块都会污染,判非法。
+
+违反任一硬约束 ⇒ 该实例记 0 分。
+
+## 5. 评分函数
+
+每块长度 `c` 的报废与贴合度:
+
+- `c < 8.0`(且非零废段小块):送不到下道工序 → 整块报废,`scrap += c`。
+- `c >= 8.0`:可送下道。超出 `target_max` 的部分报废,`scrap += c - min(c, target_max)`。
+- `target_min <= c <= target_max`:零报废、零惩罚(完美块)。
+- `c < target_min`(但 `c >= 8.0`):能送但偏短 → 零报废、贴合度惩罚。
+
+贴合度惩罚:`penalty = |min(c, target_max) - target|`(实际交付长度与目标值的距离)。
+
+指标:**材料利用率** `util = 100 * (S - (scrap + 1e-4*penalty)) / S`,多实例取平均(0~100,越高越好)。
+其中 `score = scrap + 1e-4*penalty`,`1e-4` 极小,保证**先最小化报废、再最小化贴合度**(字典序),
+`util` 略随贴合度变化以破平。
+
+> 说明:`penalty` 项只对非零废段的成品块计算;零废段小块只计入报废,不参与贴合度。
+
+## 6. 参考分数(固定 8 实例,`verification/evaluate.py` 实测)
+
+| 求解器 | 平均利用率 | 平均报废(m) |
+|---|---|---|
+| baseline(均匀等分) | 72.5 | 25.2 |
+| ref_solver(一维划分 DP,最优) | 88.7 | 10.6 |
+
+各实例参考利用率:95.8 / 88.8 / 91.8 / 75.2 / 92.4 / 87.0 / 89.4 / 89.3。
+
+> agent/框架分数(统一 low 推理):openevolve **88.7**(= 参考解)、ShinkaEvolve **88.4**、
+> AB-MCTS **72.5**(= baseline,本轮未提升)。诚实说明:**该任务的最优是可达的**——强 agent
+> 能推导出精确划分 DP 并打到 ~88.7 = 天花板;难度在"推导 DP"而非长程搜索。更难的**在线版**
+> (`CuttingOptimizationOnline`)才是信息不对称让 agent 打不满全知最优(见其 README)。
+
+## 7. 接口契约与约束
+
+- 只允许修改 `baseline/solver.py` 的 `EVOLVE-BLOCK-START` / `EVOLVE-BLOCK-END` 区域;
+ 标记外的代码必须与初始 baseline 逐字节一致。
+- 输出必须始终是合法切割方案;越界、未对齐零废段、崩溃或超时 ⇒ 该实例 0 分。
+- 允许 `import verification/simulator.py`(只读,用于搜索时评估候选方案);
+ **禁止** import / 读取 `verification/generator.py`、`verification/ref_solver.py`、
+ `verification/evaluate.py`。
+- 单实例时间预算默认 60 s,`--time-budget` 可调(挑战档 10 s)。
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/baseline/result_log.txt b/benchmarks/ContinuousCasting/CuttingOptimization/baseline/result_log.txt
new file mode 100644
index 00000000..715c2b25
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/baseline/result_log.txt
@@ -0,0 +1,31 @@
+ContinuousCasting / CuttingOptimization — baseline & reference scores (8 fixed instances)
+
+Evaluation entry: verification/evaluate.py baseline/solver.py
+Metric: material utilization = 100*(S - scrap)/S, averaged over instances (higher is better).
+Reference DP: verification/ref_solver.py (1-D partition, stdlib only).
+
+Instance set: data/instances/instance_1.json .. instance_8.json (seed-fixed).
+Mean billet length S = 94.4 m; difficulty easy/medium/hard; 0..6 defects; target window +/- 0.5 m.
+
+Per-instance (S, ref_util, base_util, ref_scrap, base_scrap):
+ i1 S= 68.9 ref= 95.79 base= 87.08 ref_scrap= 2.90 base_scrap= 8.90
+ i2 S= 50.2 ref= 88.84 base= 69.32 ref_scrap= 5.60 base_scrap= 15.40
+ i3 S=106.2 ref= 91.81 base= 75.71 ref_scrap= 8.70 base_scrap= 25.80
+ i4 S= 71.8 ref= 75.21 base= 50.14 ref_scrap= 17.80 base_scrap= 35.80
+ i5 S= 86.0 ref= 92.44 base= 69.77 ref_scrap= 6.50 base_scrap= 26.00
+ i6 S=139.7 ref= 86.97 base= 76.16 ref_scrap= 18.20 base_scrap= 33.30
+ i7 S=121.1 ref= 89.43 base= 72.83 ref_scrap= 12.80 base_scrap= 32.90
+ i8 S=111.3 ref= 89.31 base= 78.89 ref_scrap= 11.90 base_scrap= 23.50
+
+Summary:
+ baseline (equal-split, not target-aware): mean utilization = 72.49, mean scrap = 25.20 m
+ reference DP (optimal within model): mean utilization = 88.72, mean scrap = 10.55 m
+ mean headroom (ref_util - base_util) = 16.24 utilization points (reference beats baseline by this much)
+
+Every instance has headroom >= 0.1 m of scrap (generator enforces this via _interesting_ok),
+so naive equal-splitting is always strictly beatable by target-aware optimization.
+
+Agent scores (fixed 8 instances, unified low-reasoning via reasoning proxy):
+ openevolve (10 generations) = 88.72 (run 20260903_130517) == reference DP
+ ShinkaEvolve (15 generations) = 88.43 (run 20260904_182017) near-optimum
+ AB-MCTS (15 iterations) = 72.49 (run 20260904_183826) == baseline (did not improve)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/baseline/solver.py b/benchmarks/ContinuousCasting/CuttingOptimization/baseline/solver.py
new file mode 100644
index 00000000..c7b2328b
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/baseline/solver.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""连铸切割 baseline 求解器(朴素基线:每个干净坯段均匀等分)。
+
+用法:python solver.py
+输出:stdout 打印 {"cuts": [c1, c2, ...]},cuts 为切段长度列表,之和 = total_length。
+
+只允许修改 EVOLVE-BLOCK 区域内的代码;接口契约(main/stdin-json/stdout-json)必须保留。
+
+基线取"均匀等分"(朴素、不针对目标窗口):每段长度取 [min_basic, max_basic] 内的均匀值,
+但完全不考虑用户目标值 T,因此产生大量超出 target_max 的报废(或落在 target_min 下方
+的偏短块)。参考解(按目标窗口做一维划分 DP)能显著超越它,故基线是一个"正常且被明显超越"的起点。
+"""
+
+from __future__ import annotations
+
+import json
+import math
+import sys
+from pathlib import Path
+
+
+def clean_segments(inst: dict) -> list[list[float]]:
+ """干净坯段列表 [[start, length], ...](零废段之外的可切材料)。"""
+ total = float(inst["billet"]["total_length"])
+ defects = [list((a, b)) for a, b in (inst.get("defects") or [])]
+ segs: list[list[float]] = []
+ prev = 0.0
+ for a, b in defects:
+ segs.append([prev, a - prev])
+ prev = b
+ segs.append([prev, total - prev])
+ return [s for s in segs if s[1] > 1e-9]
+
+
+def solve(inst: dict) -> list[float]:
+ """返回完整钢坯的切段长度列表(含零废段小块)。"""
+ # EVOLVE-BLOCK-START
+ def partition(length: float) -> list[float]:
+ """把一段干净坯 length 等分成若干块,每块在 [min_basic, max_basic]。"""
+ a = float(inst["limits"]["min_basic"])
+ b = float(inst["limits"]["max_basic"])
+ n = max(1, math.ceil(length / b))
+ while n > 1 and length / n < a:
+ n -= 1
+ while length / n > b:
+ n += 1
+ base = length / n
+ pieces = [base] * n
+ pieces[-1] = length - base * (n - 1)
+ return [round(p, 4) for p in pieces]
+
+ segs = clean_segments(inst)
+ defects = sorted((list((a, b)) for a, b in (inst.get("defects") or [])), key=lambda x: x[0])
+ cuts: list[float] = []
+ for i, (_start, length) in enumerate(segs):
+ cuts.extend(partition(length))
+ if i < len(defects):
+ a, b = defects[i]
+ cuts.append(round(b - a, 4)) # 零废段强制切出
+ return cuts
+ # EVOLVE-BLOCK-END
+
+
+def main() -> int:
+ if len(sys.argv) < 2:
+ print("usage: python solver.py ", file=sys.stderr)
+ return 2
+ inst_path = Path(sys.argv[1])
+ inst = json.loads(inst_path.read_text(encoding="utf-8"))
+ cuts = solve(inst)
+ print(json.dumps({"cuts": cuts}))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/agent_files.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/agent_files.txt
new file mode 100644
index 00000000..640607a1
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/agent_files.txt
@@ -0,0 +1,4 @@
+README.md
+Task.md
+baseline/solver.py
+frontier_eval/constraints.txt
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/artifact_files.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/artifact_files.txt
new file mode 100644
index 00000000..a52b8b16
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/artifact_files.txt
@@ -0,0 +1,2 @@
+# No extra artifact files are auto-collected by default for this benchmark.
+# metrics.json and artifacts.json are handled separately by UnifiedTask.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/constraints.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/constraints.txt
new file mode 100644
index 00000000..8f605659
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/constraints.txt
@@ -0,0 +1,19 @@
+UnifiedTask constraints:
+1) Only modify `baseline/solver.py`, and only inside the EVOLVE-BLOCK-START / EVOLVE-BLOCK-END region.
+2) Preserve the public contract: run as `python baseline/solver.py `, print a single JSON object
+ `{"cuts": [c1, c2, ...]}` to stdout -- one cut length per piece, with the concatenation summing to
+ `total_length` (the whole steel billet, including any 0.8 m scrap segments that must be cut out).
+3) Do not modify benchmark assets, documentation, verification code, instance data, or `frontier_eval/` metadata.
+4) Your output must always be a valid cutting plan. Every cut must be in [4.8, 12.6] metres (or be exactly
+ a 0.8 m scrap-segment piece); the cuts must align to both ends of every scrap segment (scrap segments are
+ fixed waste, they must be isolated); and the sum must equal the billet length. Malformed output,
+ out-of-range cuts, unaligned cuts, crashes, or timeouts score 0 for that instance (the simulator
+ validates every plan).
+5) You may import `verification/simulator.py` (read-only) to evaluate candidate plans while searching,
+ but the plan you finally print must come from your own algorithm. You may NOT import or read
+ `verification/generator.py`, `verification/ref_solver.py`, or `verification/evaluate.py`.
+6) Prioritize validity before optimization. The solver is given a fixed time budget per instance
+ (default 60s); keep it fast enough, or your plan is discarded.
+7) Objective: minimize total scrap length first; among equal-scrap plans, make each shipped piece as close
+ to the customer target length as possible. Score = mean material utilization over instances (0-100,
+ higher is better), which equals 100 * (billet_length - scrap) / billet_length averaged.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/copy_files.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/copy_files.txt
new file mode 100644
index 00000000..79f8b1a5
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/copy_files.txt
@@ -0,0 +1,6 @@
+baseline
+verification/evaluate.py
+verification/simulator.py
+verification/validator.py
+verification/data/instances
+frontier_eval
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_command.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_command.txt
new file mode 100644
index 00000000..3a31d525
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_command.txt
@@ -0,0 +1 @@
+FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR={benchmark_source} {python} frontier_eval/run_eval.py --candidate {candidate} --metrics-out metrics.json --artifacts-out artifacts.json
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_cwd.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_cwd.txt
new file mode 100644
index 00000000..9c558e35
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/eval_cwd.txt
@@ -0,0 +1 @@
+.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/evaluator.py b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/evaluator.py
new file mode 100644
index 00000000..b4558786
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/evaluator.py
@@ -0,0 +1,36 @@
+"""Unified evaluator entry point for the CuttingOptimization benchmark.
+
+This module is loaded by `frontier_eval/run_eval.py` and must expose a
+top-level `evaluate(program_path, **kwargs)` callable. The real
+implementation lives in `verification/evaluate.py`.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+from pathlib import Path
+from typing import Any
+
+TIME_BUDGET_S = 60.0
+
+
+def _load_verification_evaluator() -> Any:
+ evaluator_path = (
+ Path(__file__).resolve().parent.parent / "verification" / "evaluate.py"
+ )
+ spec = importlib.util.spec_from_file_location(
+ "_cutting_verification_evaluator", evaluator_path
+ )
+ if spec is None or spec.loader is None:
+ raise RuntimeError(f"Failed to load verification evaluator from {evaluator_path}")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def evaluate(program_path: str, **kwargs: Any) -> Any:
+ module = _load_verification_evaluator()
+ result = module.evaluate(program_path, time_budget=TIME_BUDGET_S, **kwargs)
+ if isinstance(result, dict) and "metrics" in result:
+ return result
+ return {"metrics": result, "artifacts": {}}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/initial_program.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/initial_program.txt
new file mode 100644
index 00000000..6645b02f
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/initial_program.txt
@@ -0,0 +1 @@
+baseline/solver.py
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/readonly_files.txt b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/readonly_files.txt
new file mode 100644
index 00000000..1205de75
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/readonly_files.txt
@@ -0,0 +1,4 @@
+README.md
+Task.md
+verification
+frontier_eval
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/run_eval.py b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/run_eval.py
new file mode 100644
index 00000000..720d0e09
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/frontier_eval/run_eval.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+import traceback
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+from typing import Any
+
+INVALID_COMBINED_SCORE = -1e18
+
+
+def _write_json(path: Path, obj: Any) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(
+ json.dumps(obj, ensure_ascii=False, indent=2, default=str) + "\n",
+ encoding="utf-8",
+ )
+
+
+def _normalize_result(result: Any) -> tuple[dict[str, Any], dict[str, Any]]:
+ if hasattr(result, "metrics") and hasattr(result, "artifacts"):
+ return dict(getattr(result, "metrics")), dict(getattr(result, "artifacts"))
+
+ if isinstance(result, dict):
+ raw_metrics = result.get("metrics")
+ raw_artifacts = result.get("artifacts")
+ if isinstance(raw_metrics, dict):
+ return dict(raw_metrics), dict(raw_artifacts or {})
+ return dict(result), {}
+
+ raise TypeError(
+ "Evaluator must return an EvaluationResult-like object or a dict of metrics."
+ )
+
+
+def _load_local_evaluator() -> Any:
+ evaluator_path = Path(__file__).with_name("evaluator.py").resolve()
+ spec = spec_from_file_location("_frontier_eval_local_evaluator", evaluator_path)
+ if spec is None or spec.loader is None:
+ raise RuntimeError(f"Failed to load local evaluator from {evaluator_path}")
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+ try:
+ return getattr(module, "evaluate")
+ except AttributeError as exc:
+ raise RuntimeError(
+ f"Local evaluator does not define evaluate(): {evaluator_path}"
+ ) from exc
+
+
+def _find_repo_root() -> Path:
+ import os
+
+ env_root = os.environ.get("FRONTIER_ENGINEERING_ROOT")
+ if env_root:
+ return Path(env_root).expanduser().resolve()
+
+ here = Path(__file__).resolve()
+ for parent in [here.parent, *here.parents]:
+ if (parent / "frontier_eval").is_dir() and (parent / "benchmarks").is_dir():
+ return parent
+ return Path.cwd().resolve()
+
+
+def _build_kwargs(evaluate_fn: Any) -> dict[str, Any]:
+ kwargs: dict[str, Any] = {}
+ try:
+ parameters = inspect_signature(evaluate_fn)
+ except Exception:
+ return kwargs
+
+ if "repo_root" in parameters:
+ kwargs["repo_root"] = _find_repo_root()
+ return kwargs
+
+
+def inspect_signature(fn: Any) -> set[str]:
+ import inspect
+
+ return set(inspect.signature(fn).parameters)
+
+
+def _parse_args(argv: list[str]) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Run a benchmark-local unified evaluator and export metrics/artifacts JSON."
+ )
+ parser.add_argument("--candidate", required=True)
+ parser.add_argument("--metrics-out", default="metrics.json")
+ parser.add_argument("--artifacts-out", default="artifacts.json")
+ return parser.parse_args(argv)
+
+
+def main(argv: list[str]) -> int:
+ args = _parse_args(argv)
+
+ candidate_path = Path(args.candidate).expanduser().resolve()
+ metrics_out = Path(args.metrics_out).expanduser().resolve()
+ artifacts_out = Path(args.artifacts_out).expanduser().resolve()
+
+ metrics: dict[str, Any] = {
+ "combined_score": INVALID_COMBINED_SCORE,
+ "valid": 0.0,
+ }
+ artifacts: dict[str, Any] = {
+ "local_evaluator_path": str(Path(__file__).with_name("evaluator.py").resolve()),
+ "candidate_path": str(candidate_path),
+ }
+
+ try:
+ evaluate_fn = _load_local_evaluator()
+ result = evaluate_fn(str(candidate_path), **_build_kwargs(evaluate_fn))
+ metrics, evaluator_artifacts = _normalize_result(result)
+ artifacts.update(evaluator_artifacts)
+ except Exception as exc:
+ artifacts["error_message"] = str(exc)
+ artifacts["traceback"] = traceback.format_exc()
+
+ _write_json(metrics_out, metrics)
+ _write_json(artifacts_out, artifacts)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_1.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_1.json
new file mode 100644
index 00000000..caf2b23a
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_1.json
@@ -0,0 +1,30 @@
+{
+ "seed": 4986,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 68.9
+ },
+ "customer": {
+ "target": 9.5,
+ "target_min": 9.0,
+ "target_max": 10.0
+ },
+ "defects": [
+ [
+ 46.0,
+ 46.8
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_2.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_2.json
new file mode 100644
index 00000000..0e199e45
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_2.json
@@ -0,0 +1,30 @@
+{
+ "seed": 1996,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 50.2
+ },
+ "customer": {
+ "target": 11.1,
+ "target_min": 10.6,
+ "target_max": 11.6
+ },
+ "defects": [
+ [
+ 34.9,
+ 35.7
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_3.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_3.json
new file mode 100644
index 00000000..4263c4e8
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_3.json
@@ -0,0 +1,38 @@
+{
+ "seed": 3991,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 106.2
+ },
+ "customer": {
+ "target": 8.5,
+ "target_min": 8.0,
+ "target_max": 9.0
+ },
+ "defects": [
+ [
+ 32.4,
+ 33.2
+ ],
+ [
+ 89.9,
+ 90.7
+ ],
+ [
+ 99.1,
+ 99.9
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_4.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_4.json
new file mode 100644
index 00000000..f314861f
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_4.json
@@ -0,0 +1,38 @@
+{
+ "seed": 4,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 71.8
+ },
+ "customer": {
+ "target": 8.5,
+ "target_min": 8.0,
+ "target_max": 9.0
+ },
+ "defects": [
+ [
+ 14.3,
+ 15.1
+ ],
+ [
+ 29.1,
+ 29.9
+ ],
+ [
+ 61.2,
+ 62.0
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_5.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_5.json
new file mode 100644
index 00000000..1c6f9601
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_5.json
@@ -0,0 +1,30 @@
+{
+ "seed": 1002,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 86.0
+ },
+ "customer": {
+ "target": 9.5,
+ "target_min": 9.0,
+ "target_max": 10.0
+ },
+ "defects": [
+ [
+ 15.7,
+ 16.5
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_6.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_6.json
new file mode 100644
index 00000000..8c9a4357
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_6.json
@@ -0,0 +1,50 @@
+{
+ "seed": 6,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 139.7
+ },
+ "customer": {
+ "target": 8.5,
+ "target_min": 8.0,
+ "target_max": 9.0
+ },
+ "defects": [
+ [
+ 9.6,
+ 10.4
+ ],
+ [
+ 23.6,
+ 24.4
+ ],
+ [
+ 46.1,
+ 46.9
+ ],
+ [
+ 80.6,
+ 81.4
+ ],
+ [
+ 103.4,
+ 104.2
+ ],
+ [
+ 129.6,
+ 130.4
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_7.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_7.json
new file mode 100644
index 00000000..a1420ab0
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_7.json
@@ -0,0 +1,42 @@
+{
+ "seed": 1004,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 121.1
+ },
+ "customer": {
+ "target": 11.1,
+ "target_min": 10.6,
+ "target_max": 11.6
+ },
+ "defects": [
+ [
+ 11.5,
+ 12.3
+ ],
+ [
+ 42.6,
+ 43.4
+ ],
+ [
+ 90.3,
+ 91.1
+ ],
+ [
+ 105.6,
+ 106.4
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_8.json b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_8.json
new file mode 100644
index 00000000..a23c91c9
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/data/instances/instance_8.json
@@ -0,0 +1,42 @@
+{
+ "seed": 8,
+ "process": {
+ "speed": 1.0,
+ "cut_time": 3,
+ "return_time": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8
+ },
+ "billet": {
+ "total_length": 111.3
+ },
+ "customer": {
+ "target": 9.5,
+ "target_min": 9.0,
+ "target_max": 10.0
+ },
+ "defects": [
+ [
+ 9.2,
+ 10.0
+ ],
+ [
+ 18.6,
+ 19.4
+ ],
+ [
+ 24.3,
+ 25.1
+ ],
+ [
+ 86.7,
+ 87.5
+ ]
+ ],
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ }
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/docker/Dockerfile b/benchmarks/ContinuousCasting/CuttingOptimization/verification/docker/Dockerfile
new file mode 100644
index 00000000..7c94d870
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/docker/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.11-slim
+
+WORKDIR /workspace
+
+# The evaluator, simulator, reference solver and baseline use only the Python
+# standard library. The unified runtime mounts the benchmark sandbox into the
+# container, so no benchmark files are baked into the image. The candidate may
+# read `verification/simulator.py` (it is part of the scoring objective and is
+# allowed by the constraints), but no instance data is baked in.
+
+ENV PYTHONUNBUFFERED=1
+
+CMD ["python"]
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/evaluate.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/evaluate.py
new file mode 100644
index 00000000..7d4a8405
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/evaluate.py
@@ -0,0 +1,276 @@
+"""连铸切割评测入口。
+
+CLI:python verification/evaluate.py [--time-budget 60] [--data-dir ...]
+对每个实例:subprocess 运行候选求解器(超时 = 时间预算),解析输出 cuts,调 simulator 校验打分;
+超时/格式错/越界 -> 该实例 0 分。
+分数 = 各实例"材料利用率"的平均值(0~100,越高越好)。
+
+利用率 = 100 * (S - (scrap + lambda*penalty)) / S。废弃物越少、成品越贴合目标值,利用率越高;
+它等价于"先最小化报废、再最小化贴合度惩罚",符合赛题的字典序目标,且越高越好、跨实例可比。
+
+完整性 / 防作弊(对齐 CVRP / TelecomBackup 基准的经验):
+ * 评分前静态检查候选(EVOLVE-BLOCK 标记与标记外代码、禁引用评测/生成/参考解模块、
+ 禁绝对路径、禁按实例名硬编码)——见 verification/validator.py;
+ * 候选子进程环境剥离 FRONTIER_*/CUTTING_EVAL_*(candidate_env),封侧信道;
+ * 运行时生成实例(CUTTING_EVAL_GENERATE_SEED 设置时,评测现场按种子生成新实例,
+ 候选无法预先记忆;生成的实例只存在于临时目录,不落仓库/沙箱)。
+ * 确定性探针:跨规模选若干实例(小/中/大 + 一个生成实例)各跑两次,输出必须一致。
+
+环境变量:
+ CUTTING_EVAL_GENERATE_SEED 设置后开启运行时生成(防硬编码)
+ CUTTING_EVAL_GENERATE_COUNT 生成实例数(默认 8)
+
+对外接口(供 frontier_eval/evaluator.py 包装):
+ evaluate(program_path, *, time_budget=60.0) -> {"combined_score": float,
+ "valid": float, "per_instance": {...}}
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+from typing import Any
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+from simulator import load_instance, score # noqa: E402
+from validator import candidate_env, check_candidate, check_determinism # noqa: E402
+
+INVALID_SCORE = 0.0
+DATA_DIR = Path(__file__).resolve().parent / "data" / "instances"
+DEFAULT_GENERATE_COUNT = 8
+# 运行时生成实例的难度循环(与 generator 默认一致)
+GEN_DIFFS = ("easy", "medium", "hard", "medium", "hard", "medium", "hard", "hard")
+
+
+def _source_benchmark_dir() -> Path | None:
+ raw = os.environ.get("FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR", "").strip()
+ if raw:
+ path = Path(raw)
+ if path.is_dir():
+ return path
+ return None
+
+
+def _parse_cuts(raw: str) -> list[float] | None:
+ """解析候选 stdout 为 cuts 列表;非法返回 None。
+
+ 格式:{"cuts": [c1, c2, ...]}
+ """
+ try:
+ text = raw.strip()
+ if not text:
+ return None
+ obj = json.loads(text)
+ if isinstance(obj, dict):
+ obj = obj.get("cuts")
+ if not isinstance(obj, list) or not obj:
+ return None
+ out: list[float] = []
+ for c in obj:
+ if isinstance(c, bool) or not isinstance(c, (int, float)):
+ return None
+ if math.isnan(c) or math.isinf(c):
+ return None
+ out.append(float(c))
+ return out
+ except Exception:
+ return None
+
+
+def _utilization(inst: dict[str, Any], cuts: list[float]) -> float:
+ """给定切段方案 -> 材料利用率(0~100,越高越好)。无效则 0。"""
+ ok, m = score(inst, cuts)
+ if not ok:
+ return 0.0
+ S = float(inst["billet"]["total_length"])
+ return max(0.0, 100.0 * (S - m["score"]) / S)
+
+
+def _run_one(program_path: Path, inst_path: Path, time_budget: float,
+ python: str) -> tuple[float, dict[str, Any]]:
+ inst = load_instance(inst_path)
+ started = time.time()
+ try:
+ proc = subprocess.run(
+ [python, str(program_path), str(inst_path)],
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ errors="replace",
+ timeout=time_budget,
+ cwd=str(program_path.parent),
+ env=candidate_env(),
+ )
+ elapsed = time.time() - started
+ if proc.returncode != 0:
+ return 0.0, {"status": "crash", "stderr_tail": proc.stderr[-500:]}
+ cuts = _parse_cuts(proc.stdout)
+ if cuts is None:
+ return 0.0, {"status": "bad_output", "stdout_tail": proc.stdout[-500:]}
+ util = _utilization(inst, cuts)
+ return util, {"status": "ok", "cuts": cuts, "utilization": round(util, 2),
+ "elapsed_s": round(elapsed, 2)}
+ except subprocess.TimeoutExpired:
+ return 0.0, {"status": "timeout", "budget_s": time_budget}
+ except Exception as exc:
+ return 0.0, {"status": "error", "message": str(exc)}
+
+
+def _load_host_module(mod_name: str):
+ """从宿主 benchmark 目录加载 `verification/.py`(沙箱内不含该模块时)。"""
+ import importlib.util
+
+ src = os.environ.get("FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR", "").strip()
+ if src and Path(src).is_dir():
+ path = Path(src) / "verification" / f"{mod_name}.py"
+ if path.is_file():
+ spec = importlib.util.spec_from_file_location(f"_cc_{mod_name}", path)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+ return None
+
+
+def _generate_instances(base_seed: int, count: int, out_dir: Path) -> list[Path]:
+ """按种子现场生成新实例(防硬编码)。派生种子 base_seed*1000+i,可复现。"""
+ gen_mod = _load_host_module("generator")
+ if gen_mod is None:
+ import generator as gen_mod # 直跑(非沙箱)时的本地回退
+
+ paths: list[Path] = []
+ i = 0
+ attempts = 0
+ while len(paths) < count and attempts < count * 64:
+ inst = gen_mod.generate(base_seed * 1000 + i, GEN_DIFFS[i % len(GEN_DIFFS)])
+ i += 1
+ attempts += 1
+ if not gen_mod._interesting_ok(inst):
+ continue
+ path = out_dir / f"gen_{base_seed}_{len(paths) + 1}.json"
+ path.write_text(json.dumps(inst, ensure_ascii=False) + "\n", encoding="utf-8")
+ paths.append(path)
+ return paths
+
+
+def _select_probes(instances: list[Path], n: int = 3) -> list[Path]:
+ """跨规模选确定性探针:按排序取 小/中/大 各一(外加一个生成实例,若有)。"""
+ if len(instances) <= n:
+ return list(instances)
+ idxs = sorted({0, len(instances) // 2, len(instances) - 1})
+ probes = [instances[i] for i in idxs]
+ gen = [p for p in instances if p.name.startswith("gen_")]
+ if gen:
+ probes.append(gen[0])
+ return probes
+
+
+def evaluate(program_path: str, *, time_budget: float = 60.0,
+ data_dir: str | Path | None = None,
+ python: str | None = None) -> dict[str, Any]:
+ prog = Path(program_path).resolve()
+ if not prog.exists():
+ return {"combined_score": 0.0, "valid": 0.0, "per_instance": {},
+ "error": f"program not found: {prog}"}
+
+ # 静态完整性检查(EVOLVE-BLOCK 标记/只读区比对、禁引用、禁绝对路径、禁硬编码)
+ baseline_path = None
+ src_dir = _source_benchmark_dir()
+ if src_dir is not None:
+ candidate_baseline = src_dir / "baseline" / "solver.py"
+ if candidate_baseline.is_file():
+ baseline_path = candidate_baseline
+ violations = check_candidate(prog, baseline_path=baseline_path)
+
+ # 实例池 = 固定实例(本地 data-dir 存在时)+ 运行时生成(设了生成种子时)
+ inst_dir = Path(data_dir).resolve() if data_dir else DATA_DIR
+ instances: list[Path] = []
+ if inst_dir.is_dir():
+ instances = sorted(inst_dir.glob("instance_*.json"))
+
+ gen_seed_raw = os.environ.get("CUTTING_EVAL_GENERATE_SEED", "").strip()
+ tmp_dir: Path | None = None
+ if gen_seed_raw:
+ base_seed = 0
+ try:
+ base_seed = int(gen_seed_raw)
+ except ValueError:
+ pass
+ gen_count = DEFAULT_GENERATE_COUNT
+ try:
+ gen_count = max(0, int(os.environ.get("CUTTING_EVAL_GENERATE_COUNT", "").strip()))
+ except ValueError:
+ pass
+ if gen_count > 0:
+ tmp_dir = Path(tempfile.mkdtemp(prefix="cutting_eval_"))
+ instances.extend(_generate_instances(base_seed, gen_count, tmp_dir))
+
+ if not instances:
+ return {"combined_score": 0.0, "valid": 0.0, "per_instance": {},
+ "error": f"no instances (data_dir={inst_dir}, generate_seed={gen_seed_raw!r})"}
+
+ py = python or sys.executable
+
+ # 确定性探针:跨规模选若干实例各跑两次,输出必须一致。
+ if not violations:
+ for probe in _select_probes(instances):
+ det_ok, det_note = check_determinism(py, prog, probe, time_budget)
+ if not det_ok:
+ violations = [f"determinism check failed on {probe.name}: {det_note}"]
+ break
+
+ per_instance: dict[str, Any] = {}
+ total = 0.0
+ all_valid = True
+ for inst_path in instances:
+ if violations:
+ per_instance[inst_path.name] = {"status": "preflight_failed",
+ "reasons": violations}
+ continue
+ util, info = _run_one(prog, inst_path, time_budget, py)
+ per_instance[inst_path.name] = info
+ if util <= 0 and info.get("status") != "ok":
+ all_valid = False
+ total += util
+
+ combined = total / len(instances) if instances else 0.0
+ if tmp_dir is not None:
+ shutil.rmtree(tmp_dir, ignore_errors=True)
+ return {
+ "combined_score": round(combined, 2),
+ "valid": 1.0 if all_valid and not violations else 0.0,
+ "per_instance": per_instance,
+ "num_instances": len(instances),
+ "time_budget_s": time_budget,
+ "generate_seed": gen_seed_raw or None,
+ }
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description="连铸切割评测")
+ parser.add_argument("solver", help="候选求解器脚本路径")
+ parser.add_argument("--time-budget", type=float, default=60.0,
+ help="求解时间预算(秒),默认 60")
+ parser.add_argument("--data-dir", type=str, default=None,
+ help="实例目录(默认 verification/data/instances)")
+ parser.add_argument("--generate-seed", type=int, default=None,
+ help="运行时生成实例的种子(防硬编码;等价于设 CUTTING_EVAL_GENERATE_SEED)")
+ args = parser.parse_args(argv)
+
+ if args.generate_seed is not None:
+ os.environ["CUTTING_EVAL_GENERATE_SEED"] = str(args.generate_seed)
+ result = evaluate(args.solver, time_budget=args.time_budget, data_dir=args.data_dir)
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/generator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/generator.py
new file mode 100644
index 00000000..ec2c7522
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/generator.py
@@ -0,0 +1,202 @@
+"""连铸切割实例生成器:seed 固定,生成可复现的一维切割实例(JSON)。
+
+参数语义:
+- 钢坯是一维线段 [0, S](S = total_length)。
+- 结晶器异常在坯内产生若干"零废段"(每段长 scrap_len=0.8m),必须被切出(报废)。
+- 用户目标值 T、范围 [target_min, target_max];工艺参数与长度窗口见 LIMITS。
+- 难度由 total_length、零废段数量、目标窗口宽度共同决定。
+
+零废段位置限制:相邻零废段、零废段与线段两端之间必须留出 >= min_basic 的干净坯段,
+否则该段 < min_basic 无法成一块(不可行)。生成时强制该约束。
+"""
+
+from __future__ import annotations
+
+import json
+import random
+import sys
+from pathlib import Path
+from typing import Any
+
+if __package__ in (None, ""):
+ sys.path.insert(0, str(Path(__file__).resolve().parent)) # 保证同目录 import
+
+# 长度窗口(与 simulator.py 保持一致)
+MIN_BASIC = 4.8 # 能运走的最小长度
+MAX_BASIC = 12.6 # 能运走的最大长度
+MIN_PROCESS = 8.0 # 下道工序可接受的最小长度
+MAX_PROCESS = 11.6 # 下道工序可接受的最大长度
+
+PROCESS = {
+ "speed": 1.0, # 拉坯速度 (m/min)
+ "cut_time": 3, # 切一块耗时 (min)
+ "return_time": 1, # 切完回工作起点 (min)
+ "buffer_len": 60.0, # 结晶器中心到切割机工作起点 (m)
+ "scrap_len": 0.8, # 结晶器异常产生的零废段长度 (m)
+}
+
+
+def _clamp_times(T: float) -> tuple[float, float]:
+ return T - 0.5, T + 0.5
+
+
+def _target_options(rng: random.Random) -> tuple[float, float]:
+ """从赛题提及的目标值集合里挑一个(8.5 / 9.5 / 11.1)。"""
+ return float(rng.choice([8.5, 9.5, 11.1]))
+
+
+def generate(seed: int, difficulty: str = "medium") -> dict[str, Any]:
+ """按种子生成一个切割实例。
+
+ difficulty: "easy" | "medium" | "hard"。easy 给短坯+零废段少,
+ hard 给长坯+零废段多(且窗口更紧)。
+ """
+ rng = random.Random(seed)
+
+ if difficulty == "easy":
+ s_lo, s_hi = 24.0, 70.0
+ n_def_lo, n_def_hi = 0, 1
+ elif difficulty == "hard":
+ s_lo, s_hi = 100.0, 150.0
+ n_def_lo, n_def_hi = 3, 6
+ else: # medium
+ s_lo, s_hi = 60.0, 110.0
+ n_def_lo, n_def_hi = 1, 3
+
+ S = round(rng.uniform(s_lo, s_hi), 1)
+ T = _target_options(rng)
+ t_min, t_max = _clamp_times(T)
+
+ # 生成零废段位置:彼此、与两端之间都留出 >= MIN_BASIC 干净坯。
+ n_def = rng.randint(n_def_lo, n_def_hi)
+ scrap_len = PROCESS["scrap_len"]
+ defects: list[list[float]] = []
+ # 可用的"槽位"起点范围 [MIN_BASIC, S - MIN_BASIC - scrap_len]
+ low = MIN_BASIC
+ high = S - MIN_BASIC - scrap_len
+ attempts = 0
+ while len(defects) < n_def and attempts < 500:
+ attempts += 1
+ cand = round(rng.uniform(low, high), 1)
+ # 与已有零废段保持 >= MIN_BASIC 间距(即干净坯段 >= MIN_BASIC)
+ ok = True
+ for a, b in defects:
+ if cand < b + MIN_BASIC and a < cand + scrap_len + MIN_BASIC:
+ ok = False
+ break
+ if ok:
+ defects.append([cand, round(cand + scrap_len, 1)])
+ defects.sort()
+
+ inst: dict[str, Any] = {
+ "seed": seed,
+ "process": PROCESS,
+ "billet": {"total_length": S},
+ "customer": {"target": T, "target_min": t_min, "target_max": t_max},
+ "defects": defects,
+ "limits": {
+ "min_basic": MIN_BASIC,
+ "max_basic": MAX_BASIC,
+ "min_process": MIN_PROCESS,
+ "max_process": MAX_PROCESS,
+ },
+ }
+ return inst
+
+
+def clean_segments(inst: dict[str, Any]) -> list[list[float]]:
+ """返回干净坯段列表 [[start, length], ...]。
+
+ 零废段把 [0, S] 分成若干"干净坯段",每段内部不含零废段、
+ 可被切成若干成品。零废段本身是强制报废。
+ """
+ S = float(inst["billet"]["total_length"])
+ defects = [list((a, b)) for a, b in inst["defects"]] or []
+ segs: list[list[float]] = []
+ prev = 0.0
+ for a, b in defects:
+ segs.append([prev, a - prev])
+ prev = b
+ segs.append([prev, S - prev])
+ return [s for s in segs if s[1] > 1e-9]
+
+
+def _equal_split_scrap(inst: dict[str, Any]) -> float:
+ """朴素"均匀等分"baseline 的总报废长度(用于验收 headroom)。"""
+ import math
+
+ from simulator import piece_scrap
+
+ a = MIN_BASIC
+ b = MAX_BASIC
+ total = 0.0
+ for _start, length in clean_segments(inst):
+ n = max(1, math.ceil(length / b))
+ while n > 1 and length / n < a:
+ n -= 1
+ while length / n > b:
+ n += 1
+ base = length / n
+ pieces = [base] * n
+ pieces[-1] = length - base * (n - 1)
+ # 与 baseline/solver.py 的 partition() 完全同口径(每块 round 到 4 位)
+ for c in pieces:
+ total += piece_scrap(round(c, 4), inst)
+ # 零废段强制报废(与 baseline/solver.py 的真实报废口径一致)
+ for a0, b0 in inst["defects"] or []:
+ total += (b0 - a0)
+ return total
+
+
+def _interesting_ok(inst: dict[str, Any], min_extra: float = 0.2, min_headroom: float = 0.1) -> bool:
+ """验收:参考解必须严格优于朴素等分(存在真正的优化空间)。
+
+ 两层含义:
+ 1. 最优损失须显著超过"零废段强制报废"(即存在真正的余料损失)——
+ 若最优损失只等于零废段长度,说明实例能被"一块不多不少"地切成目标长度,
+ 没有优化空间。
+ 2. 朴素"均匀等分"baseline 须比参考解差 >= min_headroom(米)——
+ 否则该实例上 agent 无区分度(优化也拿不到收益),对 benchmark 无意义。
+ 另外保证至少存在一块干净坯段 >= min_basic(否则不可行)。
+ """
+ import ref_solver
+
+ from simulator import validate
+
+ ok_segs = clean_segments(inst)
+ if not ok_segs or max(s[1] for s in ok_segs) < MIN_BASIC:
+ return False
+ ref_cuts = ref_solver.solve(inst)["cuts"]
+ if not validate(inst, ref_cuts)[0]:
+ return False
+ ref_scrap = ref_solver.total_scrap(inst, ref_cuts)
+ defect_total = sum(b - a for a, b in inst["defects"] or [])
+ if ref_scrap < defect_total + min_extra:
+ return False
+ return _equal_split_scrap(inst) >= ref_scrap + min_headroom
+
+
+def main() -> None:
+ out_dir = Path(__file__).resolve().parent / "data" / "instances"
+ out_dir.mkdir(parents=True, exist_ok=True)
+ specs = [
+ (1, "easy"), (2, "easy"), (3, "medium"), (4, "medium"),
+ (5, "medium"), (6, "hard"), (7, "hard"), (8, "hard"),
+ ]
+ for seed, difficulty in specs:
+ inst = None
+ for trial in range(600):
+ s = seed + trial * 997
+ cand = generate(s, difficulty)
+ if _interesting_ok(cand):
+ inst = cand
+ break
+ assert inst is not None, f"no acceptable instance for slot {seed}/{difficulty}"
+ path = out_dir / f"instance_{seed}.json"
+ path.write_text(json.dumps(inst, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
+ print(f"instance_{seed}.json diff={difficulty} S={inst['billet']['total_length']} "
+ f"T={inst['customer']['target']} #defects={len(inst['defects'])} seed={inst['seed']}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/multiseed_stat.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/multiseed_stat.py
new file mode 100644
index 00000000..e6df657a
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/multiseed_stat.py
@@ -0,0 +1,87 @@
+"""多运行(多种子/多轮)agent 分数统计:对一组框架运行目录取 combined_score 的 mean±std。
+
+用法:
+ python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimization/openevolve
+ python verification/multiseed_stat.py --runs-dir "runs/**/openevolve/deepseek-v4-flash" # glob
+ python verification/multiseed_stat.py --runs-dir --pattern "*openevolve*"
+
+从每个运行目录下读 `/best/best_program_info.json` 的 `metrics.combined_score`
+(框架统一保存的 best 程序分数),对多次运行做 mean / std / min / max,
+并给出"相对 reference 的余量"(gap = reference_util - mean)。
+
+对齐 CVRP 评审点:agent 分数需多轮均值±std(而非单次),衡量稳定性与真实水平。
+纯标准库。
+"""
+
+from __future__ import annotations
+
+import argparse
+import glob
+import json
+import math
+import statistics
+from pathlib import Path
+
+DEFAULT_REFERENCE = 88.72 # 全知参考解利用率(verification/ref_solver.py),可 --reference 覆盖
+
+
+def _best_score(run_dir: Path) -> float | None:
+ for rel in ("openevolve/best/best_program_info.json",
+ "shinkaevolve/best/best_program_info.json",
+ "abmcts/best/best_program_info.json",
+ "best/best_program_info.json"):
+ p = run_dir / rel
+ if p.is_file():
+ try:
+ return float(json.loads(p.read_text(encoding="utf-8"))["metrics"]["combined_score"])
+ except Exception:
+ return None
+ return None
+
+
+def collect(runs_dir: str | Path, pattern: str | None = None) -> list[tuple[Path, float]]:
+ base = Path(runs_dir)
+ if pattern:
+ bests = sorted(Path(p) for p in glob.glob(str(base / pattern), recursive=True))
+ else:
+ bests = sorted(base.rglob("best/best_program_info.json"))
+ out: list[tuple[Path, float]] = []
+ seen: set[Path] = set()
+ for best in bests:
+ # best_program_info.json 位于 //best/ 下
+ run_dir = best.parent.parent.parent
+ if run_dir in seen:
+ continue
+ score = _best_score(run_dir)
+ if score is not None:
+ out.append((run_dir, score))
+ seen.add(run_dir)
+ return out
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="多运行 agent 分数统计")
+ parser.add_argument("--runs-dir", required=True, help="运行目录或 glob 模式")
+ parser.add_argument("--pattern", default=None, help="追加的子 glob 模式")
+ parser.add_argument("--reference", type=float, default=DEFAULT_REFERENCE,
+ help="参考解利用率(默认 88.72)")
+ args = parser.parse_args()
+
+ pairs = collect(args.runs_dir, args.pattern)
+ if not pairs:
+ print("no runs found under", args.runs_dir)
+ return 1
+
+ scores = [s for _, s in pairs]
+ mean = statistics.mean(scores)
+ std = statistics.stdev(scores) if len(scores) > 1 else 0.0
+ print(f"runs: {len(scores)}")
+ for run_dir, s in pairs:
+ print(f" {run_dir.name}: {s}")
+ print(f"mean={mean:.2f} std={std:.2f} min={min(scores):.2f} max={max(scores):.2f}")
+ print(f"gap to reference ({args.reference}): {args.reference - mean:.2f}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/ref_solver.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/ref_solver.py
new file mode 100644
index 00000000..ecd8846f
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/ref_solver.py
@@ -0,0 +1,111 @@
+"""连铸切割参考解(一维划分 DP),纯 Python 标准库。
+
+思路:零废段把钢坯 [0, S] 分成若干"干净坯段";每段独立做切割优化。
+每段用一个网格化 DP:状态 = 已覆盖长度(以 GRID 为步长),
+转移 = 选一块长度 c 属于 [min_basic, max_basic],代价 = 该块的
+ scrap + lambda*penalty。段内最小总代价即最优。
+
+注意:实例中的所有长度(S、零废段位置、scrap_len、min/max_basic)都取
+0.1 米的多倍,而 GRID=0.02,故 all 长度都是 GRID 的多倍 → DP 无舍入误差,
+参考解切段长度之和恰好等于 S(可直接通过 sum≈S 校验)。
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+from typing import Any
+
+sys.path.insert(0, str(Path(__file__).resolve().parent)) # 保证同目录 import
+
+from simulator import ( # noqa: E402
+ MAX_BASIC,
+ MIN_BASIC,
+ TARGET_PENALTY_WEIGHT,
+ clean_segments,
+ piece_penalty,
+ piece_scrap,
+)
+
+GRID = 0.02
+
+
+def _build_cost_table(inst: dict[str, Any]) -> tuple[int, int, list[float]]:
+ """返回 (min_steps, max_steps, costs);costs[s-min_steps] = 切一块 s*GRID 的代价。"""
+ min_steps = int(round(MIN_BASIC / GRID))
+ max_steps = int(round(MAX_BASIC / GRID))
+ costs = []
+ for s in range(min_steps, max_steps + 1):
+ c = s * GRID
+ costs.append(piece_scrap(c, inst) + TARGET_PENALTY_WEIGHT * piece_penalty(c, inst))
+ return min_steps, max_steps, costs
+
+
+def _optimize_segment(inst: dict[str, Any], length: float) -> list[float]:
+ """对单个干净坯段做最优切割,返回该段的切段长度列表(之和 = length)。"""
+ n = int(round(length / GRID))
+ if n <= 0:
+ return []
+ min_steps, max_steps, costs = _build_cost_table(inst)
+ INF = float("inf")
+ f = [INF] * (n + 1)
+ bp = [-1] * (n + 1)
+ f[0] = 0.0
+ for pos in range(min_steps, n + 1):
+ best = INF
+ best_s = -1
+ hi = min(max_steps, pos)
+ for s in range(min_steps, hi + 1):
+ prev = f[pos - s]
+ if prev >= INF:
+ continue
+ val = prev + costs[s - min_steps]
+ if val < best:
+ best = val
+ best_s = s
+ f[pos] = best
+ bp[pos] = best_s
+
+ if f[n] >= INF:
+ return _fallback(inst, length)
+
+ pieces: list[float] = []
+ pos = n
+ while pos > 0:
+ s = bp[pos]
+ pieces.append(round(s * GRID, 4))
+ pos -= s
+ pieces.reverse()
+ return pieces
+
+
+def _fallback(inst: dict[str, Any], length: float) -> list[float]:
+ """极少数非网格情况:尽量切成 [min_basic, max_basic] 的均匀段。"""
+ k = int(length // MAX_BASIC)
+ if k == 0:
+ return [length]
+ base = length / (k + 1)
+ if base < MIN_BASIC:
+ return [length]
+ return [round(base, 4)] * (k + 1)
+
+
+def solve(inst: dict[str, Any]) -> dict[str, Any]:
+ """返回 {'cuts': [...]}:完整钢坯的最优切段(含零废段小块)。"""
+ segs = clean_segments(inst)
+ defects = sorted((list((a, b)) for a, b in inst["defects"] or []), key=lambda x: x[0])
+ cuts: list[float] = []
+ for i, (_start, length) in enumerate(segs):
+ cuts.extend(_optimize_segment(inst, length))
+ if i < len(defects):
+ a, b = defects[i]
+ cuts.append(round(b - a, 4)) # 零废段强制切出
+ return {"cuts": cuts}
+
+
+def total_scrap(inst: dict[str, Any], cuts: list[float]) -> float:
+ """给定切段方案,返回总报废长度(用于实例验收/对比)。"""
+ from simulator import score
+
+ ok, m = score(inst, cuts)
+ return m["scrap"] if ok else float("inf")
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/requirements.txt b/benchmarks/ContinuousCasting/CuttingOptimization/verification/requirements.txt
new file mode 100644
index 00000000..4e66700e
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/requirements.txt
@@ -0,0 +1,2 @@
+# The evaluator uses only the Python standard library.
+# Runtime requirement: Python >= 3.10 (no third-party dependencies).
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/simulator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/simulator.py
new file mode 100644
index 00000000..09046df3
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/simulator.py
@@ -0,0 +1,169 @@
+"""连铸切割计分模拟器:校验候选切割方案并给出评分。
+
+规则(自编简化模型,物理语义见 Task.md / README):
+- 钢坯 = 一维线段 [0, S];零废段(缺陷)必须被切出(强制报废)。
+- 切口必须对齐每个零废段端点:任何一块"成品"都不能跨过零废段边界,
+ 否则该方案非法(零废段无法被单独切出)。
+- 每块长度必须在 [min_basic, max_basic];否则非法。
+- 评分 = 总报废长度 + lambda * 总贴合度惩罚(lambda 极小,保证字典序:
+ 先最小化报废,再在同报废下让成品长度尽量贴近目标值 T)。
+
+报废判定(对齐赛题原文):
+- c < min_process:送不到下道工序 -> 整块报废,scrap += c。
+- c >= min_process:可送下道。超出 target_max 的部分报废,scrap += c - min(c, target_max)。
+- target_min <= c <= target_max:零报废、零惩罚(完美块)。
+- c < target_min(但 c >= min_process):能送但偏短 -> 零报废、贴合度惩罚。
+
+贴合度惩罚(惩罚项,用于同报废下的区分):
+- penalty = |min(c, target_max) - T|(即"实际交付长度"与目标值的距离)。
+"""
+
+from __future__ import annotations
+
+import json
+import math
+from pathlib import Path
+from typing import Any
+
+# 默认长度窗口(与 generator.py 保持一致)
+MIN_BASIC = 4.8
+MAX_BASIC = 12.6
+MIN_PROCESS = 8.0
+MAX_PROCESS = 11.6
+# 贴合度惩罚权重(远小于 1m 报废,保证字典序:报废优先,贴合度仅破平)
+TARGET_PENALTY_WEIGHT = 1e-4
+SUM_TOL = 1e-3
+BOUND_TOL = 1e-3
+
+
+def load_instance(path: str | Path) -> dict[str, Any]:
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+def _limits(inst: dict[str, Any]) -> dict[str, float]:
+ lim = inst.get("limits", {})
+ return {
+ "min_basic": float(lim.get("min_basic", MIN_BASIC)),
+ "max_basic": float(lim.get("max_basic", MAX_BASIC)),
+ "min_process": float(lim.get("min_process", MIN_PROCESS)),
+ "max_process": float(lim.get("max_process", MAX_PROCESS)),
+ }
+
+
+def clean_segments(inst: dict[str, Any]) -> list[list[float]]:
+ """干净坯段列表 [[start, length], ...](与 generator 一致,供参考解使用)。"""
+ S = float(inst["billet"]["total_length"])
+ defects = [list((a, b)) for a, b in inst["defects"]] or []
+ segs: list[list[float]] = []
+ prev = 0.0
+ for a, b in defects:
+ segs.append([prev, a - prev])
+ prev = b
+ segs.append([prev, S - prev])
+ return [s for s in segs if s[1] > 1e-9]
+
+
+def boundaries(cuts: list[float]) -> list[float]:
+ """从 cut 长度列表求切点坐标(含 0 和 S)。"""
+ b = [0.0]
+ for c in cuts:
+ b.append(b[-1] + c)
+ return b
+
+
+def piece_scrap(c: float, inst: dict[str, Any]) -> float:
+ """单块长度 c 的报废长度(不含贴合度惩罚)。"""
+ lim = _limits(inst)
+ t_max = float(inst["customer"]["target_max"])
+ if c < lim["min_process"]:
+ return c
+ return max(0.0, c - t_max)
+
+
+def piece_penalty(c: float, inst: dict[str, Any]) -> float:
+ """单块长度 c 的实际交付长度与目标值的距离(贴合度惩罚)。"""
+ t = float(inst["customer"]["target"])
+ t_max = float(inst["customer"]["target_max"])
+ delivered = min(c, t_max)
+ return abs(delivered - t)
+
+
+def _is_defect_piece(l: float, r: float, defects: list[list[float]]) -> bool:
+ """长度区间 [l, r) 是否恰好是某个零废段(允许其小于 min_basic)。"""
+ for a, b in defects:
+ if abs(l - a) <= BOUND_TOL and abs(r - b) <= BOUND_TOL:
+ return True
+ return False
+
+
+def validate(inst: dict[str, Any], cuts: list[float]) -> tuple[bool, str]:
+ """校验候选方案:sum 近似 S、每块在 [min_basic, max_basic](零废段除外)、切口对齐零废段。"""
+ lim = _limits(inst)
+ S = float(inst["billet"]["total_length"])
+ if not cuts or any((not isinstance(c, (int, float))) or math.isnan(c) or math.isinf(c) for c in cuts):
+ return False, "empty or non-numeric cuts"
+ if any(c <= 0 for c in cuts):
+ return False, "non-positive cut"
+ if abs(sum(cuts) - S) > SUM_TOL:
+ return False, f"sum {sum(cuts)!r} != S {S!r} (tol {SUM_TOL})"
+
+ defects = [list((a, b)) for a, b in inst["defects"]] or []
+ b = boundaries(cuts)
+ for i, c in enumerate(cuts):
+ l, r = b[i], b[i + 1]
+ in_range = (lim["min_basic"] - BOUND_TOL <= c <= lim["max_basic"] + BOUND_TOL)
+ is_def = _is_defect_piece(l, r, defects)
+ if not in_range and not is_def:
+ return False, f"cut {c!r} out of [{lim['min_basic']}, {lim['max_basic']}] and not a defect"
+ # 零废段端点必须是切点(否则成品跨零废段 / 零废段被切成不可运的碎块)
+ for a, bb in defects:
+ for e in (a, bb):
+ if abs(e) > 1e-9 and abs(e - S) > 1e-9:
+ if not any(abs(p - e) <= BOUND_TOL for p in b):
+ return False, f"cut does not isolate defect endpoint {e!r}"
+ return True, "ok"
+
+
+def score(inst: dict[str, Any], cuts: list[float], *, lam: float | None = None
+ ) -> tuple[bool, dict[str, Any]]:
+ """评分:返回 (valid, metrics)。metrics = {scrap, penalty, score, cuts}。"""
+ ok, reason = validate(inst, cuts)
+ if not ok:
+ return False, {"valid": False, "reason": reason}
+
+ # 零废段以独立小段表示(0.8m):scrap(0.8)=0.8,不参与贴合度惩罚。
+ defects = [list((a, b)) for a, b in inst["defects"]] or []
+ b = boundaries(cuts)
+ total_scrap = 0.0
+ total_penalty = 0.0
+ for i, c in enumerate(cuts):
+ l, r = b[i], b[i + 1]
+ total_scrap += piece_scrap(c, inst)
+ if not _is_defect_piece(l, r, defects):
+ total_penalty += piece_penalty(c, inst)
+
+ lam = TARGET_PENALTY_WEIGHT if lam is None else float(lam)
+ return True, {
+ "valid": True,
+ "scrap": round(total_scrap, 4),
+ "penalty": round(total_penalty, 4),
+ "score": round(total_scrap + lam * total_penalty, 4),
+ "cuts": cuts,
+ }
+
+
+def solve_value(inst: dict[str, Any], cuts: list[float]) -> float:
+ """便捷路由:无效 -> 返回极大值;有效 -> 返回 score。"""
+ ok, m = score(inst, cuts)
+ return m["score"] if ok else math.inf
+
+
+if __name__ == "__main__":
+ import sys
+ if len(sys.argv) < 2:
+ print("usage: python simulator.py ", file=sys.stderr)
+ raise SystemExit(2)
+ inst = load_instance(sys.argv[1])
+ print(json.dumps({"clean_segments": clean_segments(inst),
+ "defects": inst["defects"]}, ensure_ascii=False, indent=1))
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_evaluator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_evaluator.py
new file mode 100644
index 00000000..0dc41b7f
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_evaluator.py
@@ -0,0 +1,61 @@
+"""evaluate.py 单测:完整评测链路(baseline 合法 + 作弊候选被拒绝 + 运行时生成)。"""
+
+from __future__ import annotations
+
+import json
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import evaluate # noqa: E402
+
+BASELINE = Path(__file__).resolve().parent.parent / "baseline" / "solver.py"
+
+CHEAT = '''#!/usr/bin/env python3
+from __future__ import annotations
+import json, sys
+from pathlib import Path
+
+def solve(inst):
+ # EVOLVE-BLOCK-START
+ from ref_solver import solve as ref
+ return ref(inst)["cuts"]
+ # EVOLVE-BLOCK-END
+'''
+
+
+class TestEvaluator(unittest.TestCase):
+ def test_baseline_scores_positive_and_valid(self):
+ res = evaluate.evaluate(str(BASELINE), time_budget=20)
+ self.assertEqual(res["valid"], 1.0)
+ self.assertGreater(res["combined_score"], 0.0)
+ self.assertGreater(res["num_instances"], 0)
+
+ def test_runtime_generation_adds_instances(self):
+ import os
+ os.environ["CUTTING_EVAL_GENERATE_SEED"] = "7"
+ os.environ["CUTTING_EVAL_GENERATE_COUNT"] = "3"
+ try:
+ res = evaluate.evaluate(str(BASELINE), time_budget=20)
+ finally:
+ del os.environ["CUTTING_EVAL_GENERATE_SEED"]
+ del os.environ["CUTTING_EVAL_GENERATE_COUNT"]
+ gen = [k for k in res["per_instance"] if k.startswith("gen_")]
+ self.assertEqual(len(gen), 3)
+
+ def test_cheating_candidate_rejected(self):
+ with tempfile.TemporaryDirectory() as td:
+ cheat = Path(td) / "cheat.py"
+ cheat.write_text(CHEAT, encoding="utf-8")
+ res = evaluate.evaluate(str(cheat), time_budget=20,
+ data_dir=Path(__file__).parent / "data" / "instances")
+ self.assertEqual(res["valid"], 0.0)
+ # 所有实例都应 preflight_failed(或至少 combined_score 为 0)
+ self.assertLessEqual(res["combined_score"], 0.0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_frontier_eval_evaluator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_frontier_eval_evaluator.py
new file mode 100644
index 00000000..3522d1f4
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_frontier_eval_evaluator.py
@@ -0,0 +1,103 @@
+"""frontier_eval/evaluator.py(沙箱入口)测试。
+
+对齐 CVRP 评审点:"沙箱版 evaluator 必须有测试覆盖"——测试 benchmark 的 uniform 入口
+(run_eval 加载的 evaluator.py)与 verification/evaluate.py 行为一致,且能拒绝作弊候选。
+"""
+
+from __future__ import annotations
+
+import importlib.util
+import json
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+TASK = Path(__file__).resolve().parent.parent
+
+
+def _load_evaluator():
+ p = TASK / "frontier_eval" / "evaluator.py"
+ spec = importlib.util.spec_from_file_location("_oc_sandbox_evaluator", p)
+ m = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(m)
+ return m
+
+
+def _write_cheat(d: Path) -> Path:
+ """作弊候选:在 EVOLVE-BLOCK 里 import ref_solver(应被 preflight 拒绝)。"""
+ p = d / "cheat.py"
+ p.write_text(
+ '#!/usr/bin/env python3\n'
+ 'from __future__ import annotations\n'
+ 'import json, sys\n'
+ 'def solve(inst):\n'
+ ' # EVOLVE-BLOCK-START\n'
+ ' from ref_solver import solve as ref\n'
+ ' return ref(inst)["cuts"]\n'
+ ' # EVOLVE-BLOCK-END\n'
+ 'def main():\n'
+ ' inst=json.load(open(sys.argv[1]))\n'
+ ' print(json.dumps({"cuts": solve(inst)}))\n'
+ 'if __name__ == "__main__": main()\n',
+ encoding="utf-8",
+ )
+ return p
+
+
+class TestSandboxEvaluator(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ sys.path.insert(0, str(TASK / "verification"))
+ cls.ev = _load_evaluator()
+
+ def test_baseline_scores_and_valid(self):
+ import os
+ os.environ.pop("CUTTING_EVAL_GENERATE_SEED", None)
+ r = self.ev.evaluate(str(TASK / "baseline" / "solver.py"))
+ if isinstance(r, dict) and "metrics" in r:
+ r = r["metrics"]
+ self.assertEqual(r.get("valid"), 1.0)
+ self.assertGreater(r.get("combined_score", 0.0), 0.0)
+
+ def test_runtime_generation_adds_instances(self):
+ import os
+ os.environ["CUTTING_EVAL_GENERATE_SEED"] = "7"
+ os.environ["CUTTING_EVAL_GENERATE_COUNT"] = "3"
+ try:
+ r = self.ev.evaluate(str(TASK / "baseline" / "solver.py"),
+ data_dir=str(TASK / "verification" / "data" / "instances"))
+ if isinstance(r, dict) and "metrics" in r:
+ r = r["metrics"]
+ finally:
+ os.environ.pop("CUTTING_EVAL_GENERATE_SEED", None)
+ os.environ.pop("CUTTING_EVAL_GENERATE_COUNT", None)
+ gen = [k for k in r["per_instance"] if k.startswith("gen_")]
+ self.assertEqual(len(gen), 3)
+
+ def test_cheating_candidate_rejected(self):
+ with tempfile.TemporaryDirectory() as td:
+ cheat = _write_cheat(Path(td))
+ r = self.ev.evaluate(str(cheat),
+ data_dir=str(TASK / "verification" / "data" / "instances"))
+ if isinstance(r, dict) and "metrics" in r:
+ r = r["metrics"]
+ self.assertEqual(r.get("valid"), 0.0)
+
+ def test_consistent_with_verification_evaluator(self):
+ """沙箱入口与 verification/evaluate.py 应给出同样的 combined_score(数值一致)。"""
+ import os
+ os.environ.pop("CUTTING_EVAL_GENERATE_SEED", None)
+ sandbox = self.ev.evaluate(str(TASK / "baseline" / "solver.py"),
+ data_dir=str(TASK / "verification" / "data" / "instances"))
+ if isinstance(sandbox, dict) and "metrics" in sandbox:
+ sandbox = sandbox["metrics"]
+
+ from evaluate import evaluate as ver_evaluate
+ ver = ver_evaluate(str(TASK / "baseline" / "solver.py"),
+ data_dir=str(TASK / "verification" / "data" / "instances"))
+ self.assertAlmostEqual(sandbox["combined_score"], ver["combined_score"], places=2)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_generator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_generator.py
new file mode 100644
index 00000000..7c8f20d2
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_generator.py
@@ -0,0 +1,57 @@
+"""generator.py 单测:确定性、零废段约束、干净坯段可行性、interesting 验收。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import generator # noqa: E402
+from simulator import MIN_BASIC # noqa: E402
+
+
+class TestGenerate(unittest.TestCase):
+ def test_deterministic_same_seed(self):
+ a = generator.generate(123, "medium")
+ b = generator.generate(123, "medium")
+ self.assertEqual(a, b)
+
+ def test_lengths_are_grid_multiples(self):
+ """所有长度均应接近 0.1 的整数倍(0.02 网格可整除,保证 DP 无舍入误差)。"""
+ inst = generator.generate(7, "hard")
+ S = float(inst["billet"]["total_length"])
+ self.assertAlmostEqual(round(S / 0.02) * 0.02, S, places=6)
+ for a, b in inst["defects"]:
+ self.assertEqual(abs((b - a) - 0.8) < 1e-6, True)
+
+ def test_defects_within_bounds_and_sorted(self):
+ inst = generator.generate(6, "hard")
+ S = float(inst["billet"]["total_length"])
+ prev_end = 0.0
+ for a, b in inst["defects"]:
+ self.assertGreaterEqual(a, 0.0)
+ self.assertLessEqual(b, S)
+ self.assertGreater(a, prev_end)
+ self.assertGreaterEqual(a - prev_end, MIN_BASIC - 1e-6)
+ prev_end = b
+ self.assertGreaterEqual(S - prev_end, MIN_BASIC - 1e-6)
+
+ def test_clean_segments_all_feasible(self):
+ inst = generator.generate(5, "medium")
+ segs = generator.clean_segments(inst)
+ self.assertTrue(segs)
+ for _s, length in segs:
+ self.assertGreaterEqual(length, MIN_BASIC - 1e-6)
+
+ def test_interesting_ok_on_accepted_instances(self):
+ """main() 产出的每个实例都应通过 _interesting_ok(有 headroom、非退化)。"""
+ for p in sorted(Path(__file__).parent.glob("data/instances/instance_*.json")):
+ import json
+ inst = json.loads(p.read_text(encoding="utf-8"))
+ self.assertTrue(generator._interesting_ok(inst), p.name)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_ref_solver.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_ref_solver.py
new file mode 100644
index 00000000..883978c7
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_ref_solver.py
@@ -0,0 +1,50 @@
+"""ref_solver.py 单测:求解合法性、确定性、显著优于 baseline、等于全局最优。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import ref_solver # noqa: E402
+from simulator import load_instance, score, validate # noqa: E402
+
+DATA = Path(__file__).resolve().parent / "data" / "instances"
+
+
+class TestRefSolver(unittest.TestCase):
+ def test_solution_valid_on_fixed_instances(self):
+ for p in sorted(DATA.glob("instance_*.json")):
+ inst = load_instance(p)
+ cuts = ref_solver.solve(inst)["cuts"]
+ ok, reason = validate(inst, cuts)
+ self.assertTrue(ok, f"{p.name}: {reason}")
+
+ def test_deterministic(self):
+ inst = load_instance(sorted(DATA.glob("instance_*.json"))[0])
+ self.assertEqual(ref_solver.solve(inst), ref_solver.solve(inst))
+
+ def test_equals_or_beats_equal_split(self):
+ """参考解利用率应不劣于朴素等分(一般显著更优)。"""
+ import subprocess
+ import sys as _sys
+ from simulator import score as _score
+
+ baseline = Path(__file__).resolve().parent.parent / "baseline" / "solver.py"
+ for p in sorted(DATA.glob("instance_*.json"))[:4]:
+ inst = load_instance(p)
+ ref_cuts = ref_solver.solve(inst)["cuts"]
+ out = subprocess.run(
+ [_sys.executable, str(baseline), str(p)], capture_output=True, text=True
+ )
+ import json
+ base_cuts = json.loads(out.stdout)["cuts"]
+ _, r = score(inst, ref_cuts)
+ _, b = score(inst, base_cuts)
+ self.assertLessEqual(r["score"], b["score"], f"{p.name}: ref should be <= baseline")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_simulator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_simulator.py
new file mode 100644
index 00000000..a53ac2c7
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_simulator.py
@@ -0,0 +1,124 @@
+"""simulator.py 单测:评分规则、校验逻辑、零废段处理。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import generator # noqa: E402
+from simulator import ( # noqa: E402
+ MAX_BASIC,
+ MIN_BASIC,
+ TARGET_PENALTY_WEIGHT,
+ boundaries,
+ piece_penalty,
+ piece_scrap,
+ score,
+ validate,
+)
+
+
+def _inst(seed: int = 1, difficulty: str = "medium"):
+ inst = generator.generate(seed, difficulty)
+ # 强制放入一个 0.8m 零废段,便于测试(若无则加上一个远离端部的)
+ if not inst["defects"]:
+ S = float(inst["billet"]["total_length"])
+ inst["defects"] = [[round(S / 2.0, 1), round(S / 2.0 + 0.8, 1)]]
+ return inst
+
+
+class TestPieceRules(unittest.TestCase):
+ def test_too_short_fully_scrapped(self):
+ inst = _inst()
+ self.assertEqual(piece_scrap(6.0, inst), 6.0) # < min_process(8.0) -> 整块报废
+
+ def test_in_window_zero_scrap(self):
+ inst = _inst()
+ tmin = float(inst["customer"]["target_min"])
+ tmax = float(inst["customer"]["target_max"])
+ self.assertEqual(piece_scrap((tmin + tmax) / 2.0, inst), 0.0)
+
+ def test_over_target_extra_scrapped(self):
+ inst = _inst()
+ tmax = float(inst["customer"]["target_max"])
+ self.assertAlmostEqual(piece_scrap(tmax + 1.6, inst), 1.6)
+
+ def test_penalty_is_distance_to_target(self):
+ inst = _inst()
+ t = float(inst["customer"]["target"])
+ tmax = float(inst["customer"]["target_max"])
+ self.assertAlmostEqual(piece_penalty(t, inst), 0.0)
+ # 超出 target_max 的部分会报废;实际交付长度 = min(c, target_max),惩罚 = |交付 - target|
+ self.assertAlmostEqual(piece_penalty(t + 1.0, inst), abs(min(t + 1.0, tmax) - t))
+
+
+class TestValidate(unittest.TestCase):
+ def test_valid_solution_passes(self):
+ import ref_solver
+ inst = _inst()
+ cuts = ref_solver.solve(inst)["cuts"]
+ ok, reason = validate(inst, cuts)
+ self.assertTrue(ok, reason)
+
+ def test_sum_mismatch(self):
+ inst = _inst()
+ cuts = [5.0, 5.0]
+ ok, _ = validate(inst, cuts)
+ self.assertFalse(ok)
+
+ def test_cut_out_of_range(self):
+ inst = _inst()
+ S = float(inst["billet"]["total_length"])
+ # 全部切成越界长度(< min_basic),且不覆盖零废段端点
+ cuts = [3.0] * int(round(S / 3.0))
+ ok, _ = validate(inst, cuts)
+ self.assertFalse(ok)
+
+ def test_defect_piece_allowed_below_min_basic(self):
+ import ref_solver
+ inst = _inst(seed=3, difficulty="medium")
+ self.assertTrue(inst["defects"])
+ cuts = ref_solver.solve(inst)["cuts"]
+ ok, reason = validate(inst, cuts)
+ self.assertTrue(ok, reason)
+ # 参考解应包含一个 < min_basic 的零废段块(0.8m),且合法
+ defect_len = round(inst["defects"][0][1] - inst["defects"][0][0], 4)
+ self.assertLess(defect_len, MIN_BASIC)
+ self.assertIn(defect_len, [round(c, 4) for c in cuts])
+
+ def test_crossing_defect_rejected(self):
+ inst = _inst()
+ a, b = inst["defects"][0]
+ # 找两个切口,一个在零废段内部、一个在其后,跨过零废段 -> 非法
+ piece_in = a + 0.3 # 落在零废段内
+ cuts = [piece_in, 5.0, 5.0, 5.0, 5.0]
+ # 需要校验 sum 与端点;直接断言 endpoints 之一不是边界时非法
+ ok, reason = validate(inst, cuts)
+ # 这些 cuts 大概率 sum 不匹配也非法;重点确认至少被判非法
+ self.assertFalse(ok)
+
+
+class TestScore(unittest.TestCase):
+ def test_score_is_scrap_plus_lam_penalty(self):
+ import ref_solver
+ inst = _inst()
+ cuts = ref_solver.solve(inst)["cuts"]
+ ok, m = score(inst, cuts)
+ self.assertTrue(ok)
+ # score 四舍五入到 4 位,故用 3 位精度比较
+ self.assertAlmostEqual(
+ m["score"], m["scrap"] + TARGET_PENALTY_WEIGHT * m["penalty"], places=3
+ )
+
+ def test_invalid_solution_scores_invalid(self):
+ inst = _inst()
+ ok, m = score(inst, [1.0, 1.0])
+ self.assertFalse(ok)
+ self.assertFalse(m["valid"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_validator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_validator.py
new file mode 100644
index 00000000..1b80b6a9
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/test_validator.py
@@ -0,0 +1,80 @@
+"""validator.py 单测:静态完整性检查、禁引用、绝对路径、硬编码、环境剥离。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+from validator import ( # noqa: E402
+ candidate_env,
+ check_candidate,
+ static_check_source,
+)
+
+BASELINE = Path(__file__).resolve().parent.parent / "baseline" / "solver.py"
+
+TEMPLATE = '''#!/usr/bin/env python3
+"""candidate"""
+from __future__ import annotations
+import json, sys
+from pathlib import Path
+
+def solve(inst):
+ # EVOLVE-BLOCK-START
+ return [float(inst['billet']['total_length'])]
+ # EVOLVE-BLOCK-END
+'''
+
+CHEAT_REF = TEMPLATE.replace('return [float(inst[\'billet\'][\'total_length\'])]', 'from ref_solver import solve\n return solve(inst)["cuts"]')
+CHEAT_GEN = TEMPLATE.replace('return [float(inst[\'billet\'][\'total_length\'])]', 'import generator\n return generator.generate(1)["billet"]')
+CHEAT_ABS = TEMPLATE.replace('return [float(inst[\'billet\'][\'total_length\'])]', 'open("C:\\\\Users\\\\x.txt")')
+CHEAT_HARDCODE = TEMPLATE.replace('return [float(inst[\'billet\'][\'total_length\'])]', 'return [("instance_1": [1,2])]')
+NO_MARKERS = TEMPLATE.replace('# EVOLVE-BLOCK-START\n ', '').replace('# EVOLVE-BLOCK-END\n', '')
+
+
+class TestValidator(unittest.TestCase):
+ def test_baseline_passes(self):
+ self.assertEqual(check_candidate(BASELINE, baseline_path=BASELINE), [])
+
+ def test_plain_candidate_passes(self):
+ self.assertEqual(static_check_source(TEMPLATE), [])
+
+ def test_ref_solver_import_caught(self):
+ self.assertTrue(any("ref_solver" in m for m in static_check_source(CHEAT_REF)))
+
+ def test_generator_import_caught(self):
+ self.assertTrue(any("generator" in m.lower() for m in static_check_source(CHEAT_GEN)))
+
+ def test_absolute_path_caught(self):
+ self.assertTrue(any("absolute" in m for m in static_check_source(CHEAT_ABS)))
+
+ def test_hardcode_caught(self):
+ self.assertTrue(any("hardcode" in m.lower() for m in static_check_source(CHEAT_HARDCODE)))
+
+ def test_missing_markers_caught(self):
+ self.assertTrue(any("EVOLVE-BLOCK" in m for m in static_check_source(NO_MARKERS)))
+
+ def test_fixed_region_vs_baseline(self):
+ # 修改了 EVOLVE-BLOCK 之外的代码(改 docstring)应被抓到
+ base_src = BASELINE.read_text(encoding="utf-8")
+ mod = base_src.replace("朴素基线", "朴素基线(被修改)").replace("连铸切割 baseline 求解器", "连铸切割 baseline 求解器X")
+ self.assertTrue(any("outside EVOLVE-BLOCK" in m for m in static_check_source(mod, base_src)))
+
+ def test_candidate_env_strips_vars(self):
+ import os
+ os.environ["CUTTING_EVAL_GENERATE_SEED"] = "42"
+ os.environ["FRONTIER_EVAL_SOMETHING"] = "x"
+ try:
+ env = candidate_env()
+ self.assertNotIn("CUTTING_EVAL_GENERATE_SEED", env)
+ self.assertNotIn("FRONTIER_EVAL_SOMETHING", env)
+ finally:
+ os.environ.pop("CUTTING_EVAL_GENERATE_SEED", None)
+ os.environ.pop("FRONTIER_EVAL_SOMETHING", None)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/verification/validator.py b/benchmarks/ContinuousCasting/CuttingOptimization/verification/validator.py
new file mode 100644
index 00000000..cfb0b953
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/verification/validator.py
@@ -0,0 +1,154 @@
+"""ContinuousCasting 候选完整性校验(借鉴 CVRP / TelecomBackup validator)。
+
+在评分前以"可执行检查"强制任务约束(不只是自然语言):
+ 1. EVOLVE-BLOCK 完整性:标记必须存在;标记外的代码必须与初始 baseline 逐字节一致。
+ 2. 禁引用:候选不得 import verification 的评测/生成模块(evaluate/generator/ref_solver),
+ 不得出现绝对路径,不得按实例名硬编码。
+ 3. 确定性:同一实例跑两次必须输出一致。
+ 4. candidate_env:候选子进程环境剥离 FRONTIER_*/CUTTING_EVAL_* 变量,
+ 封死"通过宿主环境变量定位评测基线"的侧信道。
+
+候选允许 import `verification/simulator.py`(白盒计分器,任务有意暴露),
+但不允许 import 评测/生成/参考解逻辑。纯标准库。
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import re
+import subprocess
+from pathlib import Path
+from typing import Any
+
+EVOLVE_START = "EVOLVE-BLOCK-START"
+EVOLVE_END = "EVOLVE-BLOCK-END"
+PROJECT_PREFIX = "CUTTING_EVAL_"
+
+# 禁引用 token:强 token 子串匹配即可(正常求解器几乎不会出现)。
+STRONG_TOKENS = (
+ "ref_solver",
+ "CUTTING_EVAL_",
+ "from generator",
+ "import generator",
+)
+# "verification" 只在与评测/生成模块构成 import/路径上下文时禁止
+# (候选被允许 import verification/simulator.py)。
+FORBIDDEN_RE = (
+ re.compile(r"verification[\\/](?:evaluate|generator|ref_solver)"),
+ re.compile(r"verification\s*\.\s*(?:evaluate|generator|ref_solver)\b"),
+ re.compile(r"\b(?:from|import)\s+(?:evaluate|generator|ref_solver)\b"),
+)
+# Windows 盘符 / POSIX 家目录绝对路径。
+ABS_PATH_RE = re.compile(r"[A-Za-z]:[\\/]|/home/|/Users/")
+# 按实例名硬编码,如 "instance_1": [...] 或 "gen_42_1": [...]
+HARDCODE_RE = re.compile(r"[\"'](?:instance|gen)_\d+(?:_\d+)?[\"']\s*:")
+
+
+def split_evolve_blocks(src: str) -> tuple[str, str, str] | None:
+ start = src.find(EVOLVE_START)
+ end = src.find(EVOLVE_END)
+ if start == -1 or end == -1 or end <= start:
+ return None
+ return (
+ src[:start],
+ src[start + len(EVOLVE_START) : end],
+ src[end + len(EVOLVE_END) :],
+ )
+
+
+def fixed_region(parts: tuple[str, str, str]) -> str:
+ """EVOLVE-BLOCK 之外的只读部分(忽略 CRLF/LF 与结尾缺换行的无损差异)。"""
+ return (parts[0] + parts[2]).replace("\r\n", "\n").rstrip("\n")
+
+
+def static_check_source(src: str, baseline_src: str | None = None) -> list[str]:
+ issues: list[str] = []
+ parts = split_evolve_blocks(src)
+ if parts is None:
+ issues.append("missing EVOLVE-BLOCK-START / EVOLVE-BLOCK-END markers")
+ elif baseline_src is not None:
+ init_parts = split_evolve_blocks(baseline_src)
+ if init_parts is not None and fixed_region(init_parts) != fixed_region(parts):
+ issues.append("code outside EVOLVE-BLOCK differs from initial baseline")
+
+ for token in STRONG_TOKENS:
+ if token in src:
+ issues.append(f"candidate references forbidden token {token!r}")
+ for pat in FORBIDDEN_RE:
+ if pat.search(src):
+ issues.append("candidate references forbidden evaluation/generation/ref module")
+
+ if ABS_PATH_RE.search(src):
+ issues.append("candidate contains an absolute filesystem path")
+ if HARDCODE_RE.search(src):
+ issues.append("candidate hardcodes per-instance schedules by name")
+
+ return issues
+
+
+def check_candidate(
+ solver_path: Path | str, baseline_path: Path | str | None = None
+) -> list[str]:
+ solver_path = Path(solver_path)
+ try:
+ src = solver_path.read_text(encoding="utf-8", errors="replace")
+ except Exception as exc:
+ return [f"cannot read candidate source: {exc}"]
+
+ baseline_src = None
+ if baseline_path is not None:
+ try:
+ baseline_src = Path(baseline_path).read_text(
+ encoding="utf-8", errors="replace"
+ )
+ except Exception:
+ baseline_src = None
+ return static_check_source(src, baseline_src)
+
+
+def candidate_env() -> dict[str, str]:
+ """候选子进程环境:剥离宿主路径与评测相关变量。"""
+ env = os.environ.copy()
+ for key in list(env):
+ upper = key.upper()
+ if upper.startswith("FRONTIER") or upper.startswith(PROJECT_PREFIX):
+ del env[key]
+ return env
+
+
+def check_determinism(
+ python: str,
+ solver_path: Path | str,
+ inst_path: Path | str,
+ timeout: float,
+) -> tuple[bool, str]:
+ """同一实例跑两次,输出必须一致。"""
+ solver_path = Path(solver_path)
+ inst_path = Path(inst_path)
+ outputs: list[Any] = []
+ for _ in range(2):
+ try:
+ proc = subprocess.run(
+ [python, str(solver_path), str(inst_path)],
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ errors="replace",
+ timeout=timeout,
+ cwd=str(solver_path.parent),
+ env=candidate_env(),
+ )
+ except subprocess.TimeoutExpired:
+ return False, "timeout during determinism check"
+ except Exception as exc:
+ return False, f"error during determinism check: {exc}"
+ if proc.returncode != 0:
+ return False, f"candidate exited with code {proc.returncode}"
+ try:
+ outputs.append(json.loads(proc.stdout))
+ except Exception as exc:
+ return False, f"cannot parse determinism output: {exc}"
+ if outputs[0] != outputs[1]:
+ return False, "candidate is not deterministic (output differs across two runs)"
+ return True, ""
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/.gitignore b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/.gitignore
new file mode 100644
index 00000000..7a60b85e
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+*.pyc
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
new file mode 100644
index 00000000..6fb4d612
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
@@ -0,0 +1,151 @@
+# CuttingOptimizationOnline: Online Optimization of Continuous-Casting Cutting (Frontier-Eng Benchmark)
+
+An **original** Frontier-Engineering benchmark extending the offline
+[CuttingOptimization](../CuttingOptimization/README.md) task with a genuinely **online**
+(closed-loop) decision process, modeled on CUMCM 2021 Problem D.
+
+A continuously cast steel billet is drawn past a cutter. **Crystallizer anomalies create
+0.8 m scrap segments, but the agent is only told about a segment when it is within
+`reveal_lead` metres of the cut line** (default 8.0 m → hidden anomalies; `reveal_lead = 60`
+is the "faithful" setting where the agent sees all relevant defects). The agent is invoked
+*once per cut decision* with the **current visible state only** — it never sees future
+defects — and must choose the next cut length. At the end the plan is scored against the
+**full hidden defect set**: any piece overlapping a scrap segment is contaminated and fully
+scrapped.
+
+The full rules and evaluation semantics are in [Task.md](./Task.md) (Chinese).
+
+## Layout
+
+```
+benchmarks/ContinuousCasting/CuttingOptimizationOnline/
+├── baseline/solver.py # Agent solver: decide(state)->piece_length (EVOLVE-BLOCK editable)
+├── verification/
+│ ├── generator.py # Seeded instance generator (HIDDEN anomaly schedule + reveal_lead)
+│ ├── simulator.py # Closed-loop simulator + contamination/scrap scoring
+│ ├── evaluate.py # Per-decision closed-loop evaluation entry
+│ ├── validator.py # Integrity checks (static + FRONTIER_* stripping + determinism)
+│ ├── ref_solver.py # Clairvoyant reference DP (sees all defects) — the ceiling
+│ ├── multiseed_stat.py # Multi-run mean±std tool
+│ ├── test_simulator.py # Unit tests: scoring / validity
+│ ├── test_generator.py # Unit tests: deterministic / hidden schedule / reveal_lead
+│ ├── test_ref_solver.py # Unit tests: reference validity + beats baseline
+│ ├── test_validator.py # Unit tests: static checks / env stripping
+│ ├── test_evaluator.py # Unit tests: closed-loop eval / cheat rejection / generation
+│ ├── test_frontier_eval_evaluator.py # Unit tests: sandbox evaluator entry
+│ ├── data/instances/ # 8 fixed instances (seed-fixed)
+│ ├── docker/Dockerfile # Minimal stdlib-only image
+│ └── requirements.txt
+├── frontier_eval/ # UnifiedTask metadata (openai-compatible LLM)
+├── Task.md # Formal model, interface, scoring, reference scores
+└── README.md
+```
+
+## Requirements
+
+- Python >= 3.10, standard library only. The benchmark itself needs no third-party deps.
+- To *run the agent search* you also need the Frontier-Eng framework + an OpenAI-compatible
+ LLM endpoint (this task uses `deepseek-v4-flash` through a local reasoning-control proxy).
+
+## Run
+
+```powershell
+# Score the baseline solver on the fixed 8-instance set (per-decision closed loop)
+python verification/evaluate.py baseline/solver.py --reveal-lead 10
+
+# Add runtime-generated instances (anti-hardcoding)
+$env:ONLINE_CUT_EVAL_GENERATE_SEED = ""
+python verification/evaluate.py baseline/solver.py
+
+# Multi-run stats (mean ± std) across a framework run dir
+python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimizationOnline/openevolve
+```
+
+### Using an OpenAI-compatible proxy (required for the agent search)
+
+`deepseek-v4-flash` is a reasoning model that can exhaust the token budget and return empty
+content unless reasoning is controlled. Two things are needed:
+
+1. **Route the LLM through a proxy** that raises `max_tokens` and injects a low
+ `reasoning_effort`, e.g. `PROXY_PORT=8765 REASONING_MODE=low MAX_TOKENS=32768 python deepseek_proxy.py`,
+ then `OPENAI_API_BASE=http://127.0.0.1:8765/v1`.
+2. **ShinkaEvolve in particular loads `.env` with `override=True`**, which clobbers
+ `OPENAI_API_BASE` and makes it bypass the proxy. **Force it via the hydra override
+ `llm.api_base=http://127.0.0.1:8765/v1`** (config value, not env). openevolve / abmcts
+ read `OPENAI_API_BASE` directly and do not need this.
+
+## Tests
+
+```powershell
+python -m unittest discover -s verification -p "test_*.py"
+```
+
+23 tests across simulator / generator / ref_solver / validator / evaluator / sandbox
+evaluator: scoring & validity (contamination, short-tail-as-scrap), deterministic generation,
+reference validity + beats-baseline, validator integrity (EVOLVE-BLOCK / forbidden refs /
+absolute paths / `FRONTIER_*` stripping / determinism), closed-loop evaluation (cheating
+candidate rejected, runtime generation), and sandbox-evaluator consistency.
+
+## Integrity / threat model
+
+- `verification/ref_solver.py` and `verification/generator.py` are **not** copied into the
+ sandbox and are additionally forbidden by the validator (`ref_solver`, `generator`,
+ `anomaly_seed` tokens).
+- Candidate subprocesses get **all `FRONTIER_*`** and `ONLINE_CUT_EVAL_*` variables stripped
+ (`validator.candidate_env`), closing the host-env side channel.
+- **Runtime generation** (`ONLINE_CUT_EVAL_GENERATE_SEED`) produces fresh instances at eval
+ time, so a candidate cannot pre-position answers. (A fixed seed is predictable if the
+ generator is public; use a fresh seed per runner for true anti-fingerprinting.)
+- Determinism probe: the closed loop is run twice on a probe instance; the cut sequences must
+ match.
+- Honest note: in process mode the candidate has host filesystem access (framework-wide
+ limitation); this benchmark relies on the layered defenses above.
+
+## Scoring
+
+- **Metric**: material utilization `util = 100 * (S - scrap) / S` averaged over instances
+ (0-100, higher better). `scrap` = contaminated pieces (any overlap with a defect → whole
+ piece scrapped) + clean pieces < 8.0 m (fully scrapped) + clean pieces above `target_max`
+ (excess scrapped), plus a tiny target-fit penalty (`1e-4 * Σ|delivered − target|`) to break
+ ties.
+- **Contamination is unavoidable**: the minimum cut is 4.8 m, so a 0.8 m defect can never be
+ isolated; it always contaminates ≥ 4.8 m of product. This is the source of the
+ "online difficulty" and the informative asymmetry.
+- **Reference scores (fixed 8 instances)**: baseline (fixed-target greedy) = **52.4**;
+ clairvoyant DP (`verification/ref_solver.py`, sees all defects) = **76.4**. The clairvoyant
+ is the theoretical ceiling — **an online agent that cannot see the future cannot reach it**.
+
+### Agent scores (final config: `reveal_lead = 10`; unified low reasoning, 15 generations each, 3 runs per framework)
+
+| Framework | run #1 | run #2 | run #3 | mean ± std |
+|---|---|---|---|---|
+| openevolve | 69.48 | 70.10 | 70.10 | **69.89 ± 0.36** |
+| shinkaevolve | 70.06 | 71.18 | 70.10 | **70.45 ± 0.64** |
+| abmcts | 72.31 | 70.10 | 70.45 | **70.95 ± 1.19** |
+
+All 9 runs combined: mean **70.43 ± 0.83**. Reference (clairvoyant, sees all defects) = **76.4**
+(gap ≈ 6.0), baseline (fixed-target greedy) = **52.4**.
+
+Observed: **every run is below the clairvoyant ceiling (76.4) and above baseline (52.4)** — i.e.
+the hidden-anomaly info asymmetry genuinely keeps online agents from the full-information optimum,
+consistently across all three frameworks. This is the **key difference vs the offline** task, where
+openevolve reaches the (offline) optimal exactly. The per-framework spread is small
+(std ≈ 0.4–1.2), so agent scores settle around the "safe short-cut" plateau (~70) that limits how
+far an online agent can get without full foresight.
+
+> Honest note: agents improve in step-jumps (stuck at baseline for several generations, then a
+> single mutation cracks ~70), not gradual climbing — consistent with a hard constraint where
+> "get the strategy right once" beats incremental search. AB-MCTS's single high run (72.31) vs its
+> std (1.19) reflects run-to-run variance, not a systematic advantage.
+
+> Honest note: agents improve in step-jumps (stuck at baseline for several generations, then a
+> single mutation cracks ~68-72), not gradual climbing — consistent with a hard constraint
+> where "get the strategy right once" beats incremental search.
+
+## Docker
+
+A minimal `python:3.11-slim` image is provided (`verification/docker/Dockerfile`). Docker
+isolation scoring depends on the shared Frontier-Eng framework's env-forwarding, which is a
+known framework-level limitation (the reference path env may not reach the container).
+`docker` isolation is therefore best verified under WSL/Linux with the unified runtime's
+`isolation_mode=docker`; on Windows hosts it is limited by a framework path bug.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
new file mode 100644
index 00000000..9edbdfd2
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
@@ -0,0 +1,116 @@
+# CuttingOptimizationOnline:连铸切割的在线优化(Frontier-Eng 基准)
+
+一个**原创**的 Frontier-Engineering 基准,是离线版
+[CuttingOptimization](../CuttingOptimization/README_zh-CN.md) 的**在线(闭环)**扩展,题材取自 2021 全国大学生数学建模竞赛 D 题。
+
+一根连续浇铸的钢坯被拉过切割机。**结晶器异常会产生 0.8m 报废段,但 agent 只在报废段距切割线 `reveal_lead` 米以内时才被告知**(默认 8m → 隐藏异常;`reveal_lead=60` 为"忠实版",此时 agent 能看到所有相关异常)。agent **每个切割决策点被调用一次、只拿到当下可见状态**,永远看不到未来,并要选下一刀长度。最后按**完整隐藏报废表**评分:任何与报废段重叠的切块都被污染、整块报废。
+
+完整规则与评测语义见 [Task.md](./Task.md)。
+
+## 目录结构
+
+```
+benchmarks/ContinuousCasting/CuttingOptimizationOnline/
+├── baseline/solver.py # agent 求解器:decide(state)->切长(EVOLVE-BLOCK 可改)
+├── verification/
+│ ├── generator.py # 种子实例生成器(隐藏异常表 + reveal_lead)
+│ ├── simulator.py # 闭环模拟器 + 污染/报废评分
+│ ├── evaluate.py # 每决策点闭环评测入口
+│ ├── validator.py # 完整性校验(静态 + FRONTIER_* 剥离 + 确定性)
+│ ├── ref_solver.py # 全知参考解 DP(看全异常)—— 理论天花板
+│ ├── multiseed_stat.py # 多轮均值±std 工具
+│ ├── test_simulator.py # 单测:评分/合法性
+│ ├── test_generator.py # 单测:确定性/隐藏表/reveal_lead
+│ ├── test_ref_solver.py # 单测:参考解合法性 + 优于基线
+│ ├── test_validator.py # 单测:静态检查/环境剥离
+│ ├── test_evaluator.py # 单测:闭环评测/作弊拒绝/生成
+│ ├── test_frontier_eval_evaluator.py # 单测:沙箱入口
+│ ├── data/instances/ # 8 个固定实例(种子固定)
+│ ├── docker/Dockerfile # 最简 python:3.11-slim 镜像
+│ └── requirements.txt
+├── frontier_eval/ # UnifiedTask 元数据(OpenAI-compatible LLM)
+├── Task.md # 正式模型、接口、评分、参考分
+└── README_zh-CN.md
+```
+
+## 运行
+
+```powershell
+# 在固定 8 实例上给 baseline 打分(每决策点闭环)
+python verification/evaluate.py baseline/solver.py --reveal-lead 10
+
+# 加运行时生成实例(防硬编码)
+$env:ONLINE_CUT_EVAL_GENERATE_SEED = ""
+python verification/evaluate.py baseline/solver.py
+
+# 多轮统计(mean±std)
+python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimizationOnline/openevolve
+```
+
+### 用 OpenAI-compatible proxy(agent 搜索必需)
+
+`deepseek-v4-flash` 是思考型模型,可能吃光 token 预算而返回空 content。需要:
+
+1. **让 LLM 走代理**:抬高 `max_tokens` 并注入低 `reasoning_effort`,如
+ `PROXY_PORT=8765 REASONING_MODE=low MAX_TOKENS=32768 python deepseek_proxy.py`,再 `OPENAI_API_BASE=http://127.0.0.1:8765/v1`。
+2. **shinkaevolve 会用 `override=True` 加载 `.env`**,把 `OPENAI_API_BASE` 覆盖掉、从而绕过代理——**必须用 hydra 覆盖 `llm.api_base=http://127.0.0.1:8765/v1`**(配置值,非环境变量)。openevolve / abmcts 直接读 `OPENAI_API_BASE`,无需此步。
+
+## 测试
+
+```powershell
+python -m unittest discover -s verification -p "test_*.py"
+```
+
+23 个单测(simulator / generator / ref_solver / validator / evaluator / 沙箱 evaluator):
+评分与合法性(污染、短尾当报废)、确定性生成、参考解合法且优于基线、validator 完整性
+(EVOLVE-BLOCK / 禁引用 / 绝对路径 / `FRONTIER_*` 剥离 / 确定性)、闭环评测(作弊候选被拒、
+运行时生成)、以及沙箱入口一致性。
+
+## 完整性 / 威胁模型
+
+- `verification/ref_solver.py` 与 `generator.py` **不**进沙箱,且被 validator 禁用
+ (`ref_solver`、`generator`、`anomaly_seed` token)。
+- 候选子进程剥离**所有 `FRONTIER_*`** 与 `ONLINE_CUT_EVAL_*` 变量(`candidate_env`),
+ 封宿主侧信道。
+- **运行时生成**(`ONLINE_CUT_EVAL_GENERATE_SEED`)评测时现场生成实例,候选无法预记忆。
+ (固定种子可预测;要真正防指纹,runner 每轮用新种子。)
+- 确定性探针:对探针实例跑两遍闭环,切段序列必须一致。
+- 诚实说明:process 模式下候选有主机文件系统访问(框架级限制);本基准依赖上述分层防御。
+
+## 评分
+
+- **指标**:材料利用率 `util = 100*(S - scrap)/S`,多实例取平均(0~100,越高越好)。
+ `scrap` = 污染块(与任一报废段重叠即整块报废)+ 干净块 <8m 整块报废 + 干净块超 `target_max`
+ 的余量报废,外加一个极小贴合度惩罚 `1e-4 * Σ|交付 - target|` 破平。
+- **污染不可避免**:最小切段 4.8m > 0.8m 报废段,报废段永远无法单独切出,必污染 ≥4.8m 成品。
+ 这正是"在线难度"与信息不对称的来源。
+- **参考分(固定 8 实例)**:baseline(恒定目标贪心)= **52.4**;全知 DP(`ref_solver.py`,
+ 看全异常)= **76.4**。全知是**理论天花板**——看不到未来的在线 agent **无法达到它**。
+
+### Agent 分(最终配置 `reveal_lead = 10`;统一 low 推理,各 15 代,每框架 3 次)
+
+| 框架 | 次1 | 次2 | 次3 | mean ± std |
+|---|---|---|---|---|
+| openevolve | 69.48 | 70.10 | 70.10 | **69.89 ± 0.36** |
+| shinkaevolve | 70.06 | 71.18 | 70.10 | **70.45 ± 0.64** |
+| abmcts | 72.31 | 70.10 | 70.45 | **70.95 ± 1.19** |
+
+9 次合计:mean **70.43 ± 0.83**;全知 reference(看全异常)= **76.4**(差 ≈6.0),baseline(恒定目标贪心)= **52.4**。
+
+观察:**每一次都低于全知天花板(76.4)、高于 baseline(52.4)**——即隐藏异常的信息不对称
+**确实让在线 agent 无法达到全知最优**,三个框架一致。这是与离线版的关键差别(离线版 openevolve
+能精确达到离线最优)。各框架 spread 小(std≈0.4–1.2),agent 分数聚在"安全短切"平台(~70)附近,
+正是"看不到未来"限制住 agent 的地方。
+
+> 诚实说明:agent 改进呈**阶梯式跳变**(卡 baseline 多代、某次突变到 ~70),不是渐进爬升——符合
+> "强约束下把策略一次想对"胜过增量搜索。AB-MCTS 那次高分(72.31)与其 std(1.19)反映的是
+> 运行间随机性,不是系统性优势。
+
+> 诚实说明:agent 改进呈**阶梯式跳变**(卡 baseline 多代、某次突变到 ~68-72),不是渐进爬升
+> ——符合"强约束下把策略一次想对"胜过增量搜索。
+
+## Docker
+
+提供最简 `python:3.11-slim` 镜像(`verification/docker/Dockerfile`)。Docker 隔离评分依赖共享
+Frontier-Eng 框架的 env 转发(已知框架级限制,参考路径 env 可能进不了容器)。`docker` 隔离最好在
+WSL/Linux 下用 unified 的 `isolation_mode=docker` 验证;Windows 宿主受框架路径 bug 限制。
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md
new file mode 100644
index 00000000..74977f31
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md
@@ -0,0 +1,80 @@
+# 连铸切割的在线优化(CuttingOptimizationOnline)
+
+## 0. 与离线版(CuttingOptimization)的区别一句话
+
+离线版给**整个钢坯 + 全部零废段位置**,agent 一次性产出静态切段,可推演精确 DP → 对强 AI 太容易(openevolve 第 8 轮即打满最优 88.72)。
+在线版是**闭环交互**:异常在运行中**按揭示距离 R 逐步暴露**,agent 在**每个切割决策点只拿到当下可见状态、看不到未来**,必须在"切长块省料"与"怕撞未知报废"之间做真实权衡。**信息不对称**是难度的唯一来源。
+
+## 1. 工艺参数(吞吐非约束,难点在信息不对称)
+
+- 拉坯速度 `v = 1.0` m/min;切一块 `tc = 3` min + 回程 `tr = 1` min;结晶器→切割机 `D = 60` m。
+- 报废段长 `scrap_len = 0.8` m。
+- 切长窗口:能运 `[min_basic, max_basic] = [4.8, 12.6]`;下道可接 `[min_process, max_process] = [8.0, 11.6]`;用户 `target` + 窗口 `[target_min, target_max]`。
+- **揭示距离 `reveal_dist = R`(默认 8.0 m)**:一段报废料仅在距离当前切割启动点 ≤ `R` 米时,才"揭示"给 agent。
+- 因 `v×(tc+tr)=4m < 4.8m`,切割机总能跟上 → **无吞吐瓶颈**。时间只在"揭示与到位"时差上起作用,不构成物理约束。
+
+> 为什么 `R < max_basic`:若把折现距离设成原题的 60m 缓冲(远大于 12.6m 切段),agent 在承诺每块之前总能看见该块内所有异常,从而永远主动避开 → 退化为离线。取 `R` 小于最大切段,使"远端未知带"存在,才产生真实不确定性。这是本任务与离线版难度分界的核心旋钮。
+
+## 2. 材料坐标与时序
+
+材料用**流坐标** `x ∈ [0, S]`(S = v·浇铸时长 = 总材料长度)。横截面 `x` 到达切割点时间为 `x/v + D/v`。正被切割的横截面位置记作 `cut_pos`(也是下一段的起点),初始 0。
+
+异常由隐藏种子确定:一段报废料占据流区间 `[x_a, x_a+0.8]`。它在**距切割点 ≤ R 时揭示**(即当 `x_a ≤ cut_pos + R`),并将其位置/到达时间加入 agent 可见列表。
+
+## 3. 求解接口(闭环 REPL)
+
+agent 程序**启动一次**:`python solver.py `(instance 里**不含任何异常/异常种子**),进入循环:
+
+- 从 stdin 读一行**状态 JSON**:
+```json
+{"cut_pos": 12.6, "committed": [9.1, 11.4], "visible_defects": [{"x": 20.0}], "target": 9.5,
+ "target_min": 9.0, "target_max": 10.0, "limits": {"min_basic":4.8,"max_basic":12.6,"min_process":8.0,"max_process":11.6},
+ "total_length": 106.2}
+```
+ - `cut_pos`:当前切割启动点(下一段的起点)。
+ - `committed`:已承诺的切段长度列表(之和 = cut_pos)。
+ - `visible_defects`:**已揭示**(距 cut_pos ≤ R)且尚未被切过/尚未过去的报废段流位置(只含 `x_a`,长度固定 0.8m)。
+ - `reveal_dist`、`total_length`、process 等。
+- 向 stdout 写一行**决策 JSON**:`{"piece_length": L}`,`L ∈ [4.8, 12.6]`。
+- 评审器按流推进:`cut_pos += L`,重复喂状态/拿下一刀;当 `S - cut_pos ≤ 12.6` 时强制收尾(最后一段 = `S - cut_pos`,agent 无需作答)。
+
+agent **永远看不到** `x_a > cut_pos + R` 的异常(未揭示);也看不到完整报废表。`visible_defects` 只含已揭示的。
+
+## 4. 污染判定(不可规避的代价,难度来源)
+
+由于最小切段 4.8m > 0.8m,0.8m 报废段无法被单独切出。因此:
+- 任何**流区间与任一报废区间重叠**的已承诺切段 → **整块污染,全部报废**(报废长度 = 整块长度)。
+- 每个报废段必然污染 ≥1 块(无解)→ 报废有**下限**;agent 要尽量让被污染块短、并让其余干净块进目标窗口。
+- 若 agent 切长块 > R,而远端 `(cut_pos+R, cut_pos+L]` 内恰好有未揭示报废料 → **惊喜污染**,这是信息不对称的惩罚。
+
+## 5. 评分(确定性)
+
+整根材料切完:
+- `scrap` = 所有污染块长度之和 +(干净块)`< min_process` 整块报废、超 `target_max` 部分报废、窗口外偏短仅贴合度惩罚。
+- 贴合度惩罚 `penalty = Σ|min(块长, target_max) − target|`(只对干净块)。
+- 指标:`util = 100 × (S − scrap) / S`,多实例平均(0~100,越高越好),外加一个极小贴合度
+ 惩罚破平。**不设调整次数惩罚**(原题并没说"调整多不好",动态调整是正常响应,故不计入)。
+
+seed 固定 → 报废表与揭示序列确定 → 模拟与评分可复现、纯标准库。
+
+## 6. 三层参照(区分度结构)
+
+| 求解器 | 信息 | 预期 |
+|---|---|---|
+| baseline(贪心恒定/不看异常) | 无(或不理会) | 底部 |
+| **agent(在线,限视 R)** | 只看到 R 内 | 中间:**低于全知参考解**(看不到未来) |
+| reference(全知 clairvoyant DP) | 看全报废表 | 理论最优 → 天花板 |
+
+**关键**:agent 因限视 **打不满参考解**——这正是"在线比离线难"的体现(离线 agent 能推 DP
+打满最优;在线 agent 做不到全知)。
+
+## 7. 既定设置(已定稿)
+
+- `reveal_lead`(揭示提前量,米/分钟)是实例 `process` 参数。**默认取 `10.0`**(隐藏异常,
+ 制造信息不对称);`60.0` 为"忠实版"(此时 agent 能看到所有相关异常,接近离线)。评估入口
+ `verification/evaluate.py --reveal-lead ` 可覆盖。
+- **接口为"每决策点一次子进程调用"**:`python solver.py ` → 打印 `{"piece_length": L}`
+ (`state` 只含 `cut_pos`/`committed`/`visible_defects`/target/limits/`reveal_lead` 等,不含未来
+ 异常;`visible_defects` 只含距 `cut_pos` ≤ `reveal_lead` 的已揭示报废段)。评估器在时间线上
+ 每个决策点新建一次子进程喂状态。
+- metrics:`util` 为主 + 极小权重×贴合度。(不带调整次数惩罚。)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/result_log.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/result_log.txt
new file mode 100644
index 00000000..db93b932
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/result_log.txt
@@ -0,0 +1,28 @@
+CuttingOptimizationOnline — baseline & reference scores (8 fixed instances, online closed-loop).
+Final config: reveal_lead = 10.0 (hidden anomalies → genuine online info asymmetry).
+
+Metric: material utilization = 100*(S - scrap)/S, averaged over instances (higher better).
+Reference (clairvoyant DP, sees all defects) = theoretical ceiling.
+Baseline (fixed-target greedy) = floor.
+
+ baseline = 52.4
+ reference (clairvoyant, all-foresight) = 76.4 <- ceiling an online agent cannot reach
+
+Agent scores (unified low-reasoning via proxy, 15 generations each, 3 runs per framework):
+
+ Framework | run1 | run2 | run3 | mean ± std
+ -------------|-------|-------|-------|-----------
+ openevolve | 69.48 | 70.10 | 70.10 | 69.89 ± 0.36
+ shinkaevolve | 70.06 | 71.18 | 70.10 | 70.45 ± 0.64
+ abmcts | 72.31 | 70.10 | 70.45 | 70.95 ± 1.19
+
+ All 9 runs: mean 70.43 ± 0.83.
+
+Key finding: every run is below the clairvoyant ceiling (76.4) and above baseline (52.4) —
+the hidden-anomaly info asymmetry keeps online agents from the full-information optimum
+consistently across all three frameworks. This is the key difference vs the offline task
+(where openevolve reaches the offline optimum exactly = 88.72).
+
+Frameworks were run with the OpenAI-compatible reasoning-control proxy at REASONING_MODE=low;
+ShinkaEvolve additionally needs the hydra override `llm.api_base=http://127.0.0.1:8765/v1`
+(its .env override otherwise makes it bypass the proxy and return empty content).
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/solver.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/solver.py
new file mode 100644
index 00000000..9b924ceb
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/baseline/solver.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""在线切割 baseline 求解器(朴素:恒定切目标值,无视异常)。
+
+用法:python solver.py
+读取一个状态 JSON(决策点),向 stdout 打印 {"piece_length": L}(L ∈ [min_basic, max_basic])。
+本 solver 由评估器在时间线上每个决策点调用一次;state 只含"已揭示"异常。
+
+只允许修改 EVOLVE-BLOCK 区域内的代码;接口契约(main/JSON/JSON)必须保留。
+
+基线是"恒定切目标值、不在意报废段":从不根据 visible_defects 调整长度。
+在会出现异常的实例上,它常把报废段切进整块成品里(或切出超/欠目标),
+因此明显弱于"看到异常勤快避让"的 agent,更弱于全知参考解。
+"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+
+def decide(state: dict) -> float:
+ """给定一个决策点状态,返回下一刀切段长度。"""
+ # EVOLVE-BLOCK-START
+ # 朴素策略:恒定切到用户目标值,并夹到 [min_basic, max_basic]。
+ # 完全不看 state["visible_defects"](不理会报废段),因此容易踩雷。
+ t = float(state.get("target", 9.5))
+ lo = float(state["limits"]["min_basic"])
+ hi = float(state["limits"]["max_basic"])
+ total = float(state["total_length"])
+ cut_pos = float(state["cut_pos"])
+ remaining = total - cut_pos
+ # 若剩余可直接作为最后一段,则切剩余(保证合法尾段)。
+ if remaining <= hi:
+ return max(lo, remaining)
+ cand = max(lo, min(hi, t))
+ # 避免留下 (0, lo) 的非法尾段:缩短当前块,让余段恰好为 lo。
+ if 0 < remaining - cand < lo:
+ cand = remaining - lo
+ if cand < lo:
+ cand = remaining # 退化为整段(此时 remaining 应 >= lo)
+ return max(lo, min(hi, cand))
+ # EVOLVE-BLOCK-END
+
+
+def main() -> int:
+ if len(sys.argv) < 2:
+ print("usage: python solver.py ", file=sys.stderr)
+ return 2
+ state = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
+ print(json.dumps({"piece_length": decide(state)}))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/agent_files.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/agent_files.txt
new file mode 100644
index 00000000..640607a1
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/agent_files.txt
@@ -0,0 +1,4 @@
+README.md
+Task.md
+baseline/solver.py
+frontier_eval/constraints.txt
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/artifact_files.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/artifact_files.txt
new file mode 100644
index 00000000..bc02d42d
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/artifact_files.txt
@@ -0,0 +1,2 @@
+# No extra artifact files are auto-collected by default.
+# metrics.json and artifacts.json are handled separately by UnifiedTask.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/constraints.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/constraints.txt
new file mode 100644
index 00000000..a74a9dd1
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/constraints.txt
@@ -0,0 +1,22 @@
+UnifiedTask constraints (online cutting, closed-loop):
+1) Only modify `baseline/solver.py`, and only inside the EVOLVE-BLOCK-START / EVOLVE-BLOCK-END region.
+2) Public contract: the evaluator drives you on a timeline. It calls `python baseline/solver.py `
+ and expects a single JSON object {"piece_length": L} on stdout, where L is a cut length in
+ [limits.min_basic, limits.max_basic] (metres) for the NEXT piece starting at state.cut_pos.
+ You are invoked once per cutting decision, in order, and receive only the defects that have
+ ALREADY been revealed (state.visible_defects). You CANNOT see future defects — that is the
+ core of the online task. State fields: cut_pos, total_length, target/target_min/target_max,
+ limits, reveal_lead, visible_defects, committed (the lengths already cut).
+3) Do not modify benchmark assets, documentation, verification code, instance data, or `frontier_eval/` metadata.
+4) Every returned L must be a valid cut. Malformed output, non-numeric/infinite L, crashes, or timeouts
+ cause that decision to fall back and score worse. The final plan is validated against the FULL
+ (hidden) defect set: any piece overlapping a 0.8 m scrap segment is fully scrapped (contamination),
+ pieces < 8.0 m are fully scrapped, pieces above target_max incur excess scrap, and target-fit is
+ measured by |min(piece, target_max) - target|.
+5) You may import `verification/simulator.py` (read-only) to evaluate candidate decisions while searching,
+ but the decision you print must come from your own algorithm. You may NOT import or read
+ `verification/generator.py`, `verification/ref_solver.py`, or `verification/evaluate.py`.
+6) Objective: minimize total scrap first (contaminated + short + over-window), then make shipped pieces
+ close to the target. Score = mean material utilization 100*(total_length - scrap)/total_length over
+ instances (0-100, higher is better). Because you cannot see the future, a clairvoyant reference
+ solver (which sees all defects) is the theoretical ceiling and is unreachable by an optimal online agent.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt
new file mode 100644
index 00000000..79f8b1a5
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt
@@ -0,0 +1,6 @@
+baseline
+verification/evaluate.py
+verification/simulator.py
+verification/validator.py
+verification/data/instances
+frontier_eval
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_command.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_command.txt
new file mode 100644
index 00000000..3a31d525
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_command.txt
@@ -0,0 +1 @@
+FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR={benchmark_source} {python} frontier_eval/run_eval.py --candidate {candidate} --metrics-out metrics.json --artifacts-out artifacts.json
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_cwd.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_cwd.txt
new file mode 100644
index 00000000..9c558e35
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/eval_cwd.txt
@@ -0,0 +1 @@
+.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/evaluator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/evaluator.py
new file mode 100644
index 00000000..53deec30
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/evaluator.py
@@ -0,0 +1,31 @@
+"""Unified evaluator entry point for the CuttingOptimizationOnline benchmark.
+
+Loaded by `frontier_eval/run_eval.py`; exposes top-level `evaluate(program_path, **kwargs)`.
+Real implementation lives in `verification/evaluate.py`.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+from pathlib import Path
+from typing import Any
+
+TIME_BUDGET_S = 60.0
+
+
+def _load_verification_evaluator() -> Any:
+ evaluator_path = (Path(__file__).resolve().parent.parent / "verification" / "evaluate.py")
+ spec = importlib.util.spec_from_file_location("_oc_verification_evaluator", evaluator_path)
+ if spec is None or spec.loader is None:
+ raise RuntimeError(f"Failed to load verification evaluator from {evaluator_path}")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def evaluate(program_path: str, **kwargs: Any) -> Any:
+ module = _load_verification_evaluator()
+ result = module.evaluate(program_path, time_budget=TIME_BUDGET_S, **kwargs)
+ if isinstance(result, dict) and "metrics" in result:
+ return result
+ return {"metrics": result, "artifacts": {}}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/initial_program.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/initial_program.txt
new file mode 100644
index 00000000..6645b02f
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/initial_program.txt
@@ -0,0 +1 @@
+baseline/solver.py
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/readonly_files.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/readonly_files.txt
new file mode 100644
index 00000000..1205de75
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/readonly_files.txt
@@ -0,0 +1,4 @@
+README.md
+Task.md
+verification
+frontier_eval
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/run_eval.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/run_eval.py
new file mode 100644
index 00000000..720d0e09
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/run_eval.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+import traceback
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+from typing import Any
+
+INVALID_COMBINED_SCORE = -1e18
+
+
+def _write_json(path: Path, obj: Any) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(
+ json.dumps(obj, ensure_ascii=False, indent=2, default=str) + "\n",
+ encoding="utf-8",
+ )
+
+
+def _normalize_result(result: Any) -> tuple[dict[str, Any], dict[str, Any]]:
+ if hasattr(result, "metrics") and hasattr(result, "artifacts"):
+ return dict(getattr(result, "metrics")), dict(getattr(result, "artifacts"))
+
+ if isinstance(result, dict):
+ raw_metrics = result.get("metrics")
+ raw_artifacts = result.get("artifacts")
+ if isinstance(raw_metrics, dict):
+ return dict(raw_metrics), dict(raw_artifacts or {})
+ return dict(result), {}
+
+ raise TypeError(
+ "Evaluator must return an EvaluationResult-like object or a dict of metrics."
+ )
+
+
+def _load_local_evaluator() -> Any:
+ evaluator_path = Path(__file__).with_name("evaluator.py").resolve()
+ spec = spec_from_file_location("_frontier_eval_local_evaluator", evaluator_path)
+ if spec is None or spec.loader is None:
+ raise RuntimeError(f"Failed to load local evaluator from {evaluator_path}")
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+ try:
+ return getattr(module, "evaluate")
+ except AttributeError as exc:
+ raise RuntimeError(
+ f"Local evaluator does not define evaluate(): {evaluator_path}"
+ ) from exc
+
+
+def _find_repo_root() -> Path:
+ import os
+
+ env_root = os.environ.get("FRONTIER_ENGINEERING_ROOT")
+ if env_root:
+ return Path(env_root).expanduser().resolve()
+
+ here = Path(__file__).resolve()
+ for parent in [here.parent, *here.parents]:
+ if (parent / "frontier_eval").is_dir() and (parent / "benchmarks").is_dir():
+ return parent
+ return Path.cwd().resolve()
+
+
+def _build_kwargs(evaluate_fn: Any) -> dict[str, Any]:
+ kwargs: dict[str, Any] = {}
+ try:
+ parameters = inspect_signature(evaluate_fn)
+ except Exception:
+ return kwargs
+
+ if "repo_root" in parameters:
+ kwargs["repo_root"] = _find_repo_root()
+ return kwargs
+
+
+def inspect_signature(fn: Any) -> set[str]:
+ import inspect
+
+ return set(inspect.signature(fn).parameters)
+
+
+def _parse_args(argv: list[str]) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Run a benchmark-local unified evaluator and export metrics/artifacts JSON."
+ )
+ parser.add_argument("--candidate", required=True)
+ parser.add_argument("--metrics-out", default="metrics.json")
+ parser.add_argument("--artifacts-out", default="artifacts.json")
+ return parser.parse_args(argv)
+
+
+def main(argv: list[str]) -> int:
+ args = _parse_args(argv)
+
+ candidate_path = Path(args.candidate).expanduser().resolve()
+ metrics_out = Path(args.metrics_out).expanduser().resolve()
+ artifacts_out = Path(args.artifacts_out).expanduser().resolve()
+
+ metrics: dict[str, Any] = {
+ "combined_score": INVALID_COMBINED_SCORE,
+ "valid": 0.0,
+ }
+ artifacts: dict[str, Any] = {
+ "local_evaluator_path": str(Path(__file__).with_name("evaluator.py").resolve()),
+ "candidate_path": str(candidate_path),
+ }
+
+ try:
+ evaluate_fn = _load_local_evaluator()
+ result = evaluate_fn(str(candidate_path), **_build_kwargs(evaluate_fn))
+ metrics, evaluator_artifacts = _normalize_result(result)
+ artifacts.update(evaluator_artifacts)
+ except Exception as exc:
+ artifacts["error_message"] = str(exc)
+ artifacts["traceback"] = traceback.format_exc()
+
+ _write_json(metrics_out, metrics)
+ _write_json(artifacts_out, artifacts)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_1.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_1.json
new file mode 100644
index 00000000..8b6f57ba
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_1.json
@@ -0,0 +1,33 @@
+{
+ "seed": 1,
+ "anomaly_seed": 100001,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 75.77,
+ "t_cast": 195.77
+ },
+ "customer": {
+ "target": 8.5,
+ "target_min": 8.0,
+ "target_max": 9.0
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 28.96,
+ 29.76
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_2.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_2.json
new file mode 100644
index 00000000..b919e8e7
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_2.json
@@ -0,0 +1,33 @@
+{
+ "seed": 2,
+ "anomaly_seed": 100002,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 67.88,
+ "t_cast": 187.88
+ },
+ "customer": {
+ "target": 11.1,
+ "target_min": 10.6,
+ "target_max": 11.6
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 58.29,
+ 59.09
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_3.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_3.json
new file mode 100644
index 00000000..4f641a58
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_3.json
@@ -0,0 +1,41 @@
+{
+ "seed": 3,
+ "anomaly_seed": 100003,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 97.86,
+ "t_cast": 217.86
+ },
+ "customer": {
+ "target": 11.1,
+ "target_min": 10.6,
+ "target_max": 11.6
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 18.37,
+ 19.17
+ ],
+ [
+ 34.7,
+ 35.5
+ ],
+ [
+ 57.19,
+ 57.99
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_4.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_4.json
new file mode 100644
index 00000000..b4a1b5ad
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_4.json
@@ -0,0 +1,53 @@
+{
+ "seed": 4,
+ "anomaly_seed": 100004,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 118.25,
+ "t_cast": 238.25
+ },
+ "customer": {
+ "target": 8.5,
+ "target_min": 8.0,
+ "target_max": 9.0
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 9.89,
+ 10.69
+ ],
+ [
+ 24.89,
+ 25.69
+ ],
+ [
+ 31.28,
+ 32.08
+ ],
+ [
+ 39.15,
+ 39.95
+ ],
+ [
+ 63.81,
+ 64.61
+ ],
+ [
+ 108.41,
+ 109.21
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_5.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_5.json
new file mode 100644
index 00000000..67e24107
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_5.json
@@ -0,0 +1,45 @@
+{
+ "seed": 5,
+ "anomaly_seed": 100005,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 99.64,
+ "t_cast": 219.64
+ },
+ "customer": {
+ "target": 11.1,
+ "target_min": 10.6,
+ "target_max": 11.6
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 19.32,
+ 20.12
+ ],
+ [
+ 47.35,
+ 48.15
+ ],
+ [
+ 56.35,
+ 57.15
+ ],
+ [
+ 70.55,
+ 71.35
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_6.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_6.json
new file mode 100644
index 00000000..081ccdfe
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_6.json
@@ -0,0 +1,77 @@
+{
+ "seed": 6,
+ "anomaly_seed": 100006,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 154.74,
+ "t_cast": 274.74
+ },
+ "customer": {
+ "target": 11.1,
+ "target_min": 10.6,
+ "target_max": 11.6
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 8.86,
+ 9.66
+ ],
+ [
+ 15.1,
+ 15.9
+ ],
+ [
+ 20.72,
+ 21.52
+ ],
+ [
+ 33.13,
+ 33.93
+ ],
+ [
+ 45.85,
+ 46.65
+ ],
+ [
+ 53.46,
+ 54.26
+ ],
+ [
+ 71.21,
+ 72.01
+ ],
+ [
+ 91.21,
+ 92.01
+ ],
+ [
+ 105.29,
+ 106.09
+ ],
+ [
+ 119.26,
+ 120.06
+ ],
+ [
+ 138.01,
+ 138.81
+ ],
+ [
+ 148.16,
+ 148.96
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_7.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_7.json
new file mode 100644
index 00000000..db997b94
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_7.json
@@ -0,0 +1,69 @@
+{
+ "seed": 7,
+ "anomaly_seed": 100007,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 135.88,
+ "t_cast": 255.88
+ },
+ "customer": {
+ "target": 9.5,
+ "target_min": 9.0,
+ "target_max": 10.0
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 26.1,
+ 26.9
+ ],
+ [
+ 32.25,
+ 33.05
+ ],
+ [
+ 43.39,
+ 44.19
+ ],
+ [
+ 52.27,
+ 53.07
+ ],
+ [
+ 63.58,
+ 64.38
+ ],
+ [
+ 80.37,
+ 81.17
+ ],
+ [
+ 96.62,
+ 97.42
+ ],
+ [
+ 102.98,
+ 103.78
+ ],
+ [
+ 113.33,
+ 114.13
+ ],
+ [
+ 125.97,
+ 126.77
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_8.json b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_8.json
new file mode 100644
index 00000000..dae0b817
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/data/instances/instance_8.json
@@ -0,0 +1,77 @@
+{
+ "seed": 8,
+ "anomaly_seed": 100008,
+ "process": {
+ "v": 1.0,
+ "tc": 3,
+ "tr": 1,
+ "buffer_len": 60.0,
+ "scrap_len": 0.8,
+ "reveal_lead": 10.0
+ },
+ "cast": {
+ "total_length": 157.99,
+ "t_cast": 277.99
+ },
+ "customer": {
+ "target": 8.5,
+ "target_min": 8.0,
+ "target_max": 9.0
+ },
+ "limits": {
+ "min_basic": 4.8,
+ "max_basic": 12.6,
+ "min_process": 8.0,
+ "max_process": 11.6
+ },
+ "defects": [
+ [
+ 5.18,
+ 5.98
+ ],
+ [
+ 30.12,
+ 30.92
+ ],
+ [
+ 37.6,
+ 38.4
+ ],
+ [
+ 58.52,
+ 59.32
+ ],
+ [
+ 64.85,
+ 65.65
+ ],
+ [
+ 74.07,
+ 74.87
+ ],
+ [
+ 85.96,
+ 86.76
+ ],
+ [
+ 92.63,
+ 93.43
+ ],
+ [
+ 121.45,
+ 122.25
+ ],
+ [
+ 134.09,
+ 134.89
+ ],
+ [
+ 140.08,
+ 140.88
+ ],
+ [
+ 151.43,
+ 152.23
+ ]
+ ]
+}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/docker/Dockerfile b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/docker/Dockerfile
new file mode 100644
index 00000000..7c94d870
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/docker/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.11-slim
+
+WORKDIR /workspace
+
+# The evaluator, simulator, reference solver and baseline use only the Python
+# standard library. The unified runtime mounts the benchmark sandbox into the
+# container, so no benchmark files are baked into the image. The candidate may
+# read `verification/simulator.py` (it is part of the scoring objective and is
+# allowed by the constraints), but no instance data is baked in.
+
+ENV PYTHONUNBUFFERED=1
+
+CMD ["python"]
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
new file mode 100644
index 00000000..f8c1ec26
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
@@ -0,0 +1,223 @@
+"""在线切割评测入口(闭环 per-decision 驱动)。
+
+每个实例:评测器在时间线上推进,每到一个切割决策点就:
+ 1) 用 simulator._make_state 构造"agent 可见状态"(只含已揭示异常,不含 future/hidden);
+ 2) 把状态写入临时文件,`subprocess` 调用 `python `;
+ 3) 解析 {"piece_length": L} 作为下一刀长度,落账推进。
+直到整根材料切完;最后用完整 defects 表打分(污染/干净规则)。
+
+评分 = 各实例材料利用率均值(0~100,越高越好)。
+防作弊/确定性/运行时生成 对齐离线版(见 validator.py)。
+
+环境变量:
+ ONLINE_CUT_EVAL_GENERATE_SEED 设置后开启运行时生成实例(防硬编码)
+ ONLINE_CUT_EVAL_GENERATE_COUNT 生成实例数(默认 8)
+
+对外接口(供 frontier_eval/evaluator.py 包装):
+ evaluate(program_path, *, time_budget=60.0) -> {"combined_score", "valid", "per_instance"}
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+from typing import Any
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+from simulator import load_instance, partition, score # noqa: E402
+from validator import candidate_env, check_candidate # noqa: E402
+
+INVALID_SCORE = 0.0
+DATA_DIR = Path(__file__).resolve().parent / "data" / "instances"
+DEFAULT_GENERATE_COUNT = 8
+GEN_DIFFS = ("easy", "medium", "hard", "medium", "hard", "medium", "hard", "hard")
+
+
+def _source_benchmark_dir() -> Path | None:
+ raw = os.environ.get("FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR", "").strip()
+ if raw and Path(raw).is_dir():
+ return Path(raw)
+ return None
+
+
+class AgentClient:
+ """把 agent 子进程封装成 decision_fn(state)->length(每决策一次调用)。"""
+
+ def __init__(self, program_path: Path, python: str, time_budget: float, tmp: Path):
+ self.program_path = program_path
+ self.python = python
+ self.time_budget = time_budget
+ self.tmp = tmp
+
+ def decide(self, state: dict[str, Any]) -> float:
+ state_path = self.tmp / f"state_{id(state)}.json"
+ state_path.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
+ try:
+ proc = subprocess.run(
+ [self.python, str(self.program_path), str(state_path)],
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
+ timeout=self.time_budget, cwd=str(self.program_path.parent),
+ env=candidate_env(),
+ )
+ except subprocess.TimeoutExpired:
+ return float("nan")
+ if proc.returncode != 0:
+ return float("nan")
+ try:
+ obj = json.loads(proc.stdout)
+ return float(obj["piece_length"])
+ except Exception:
+ return float("nan")
+
+
+def _load_host_module(mod_name: str):
+ import importlib.util
+ src = os.environ.get("FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR", "").strip()
+ if src and Path(src).is_dir():
+ p = Path(src) / "verification" / f"{mod_name}.py"
+ if p.is_file():
+ spec = importlib.util.spec_from_file_location(f"_oc_{mod_name}", p)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+ return None
+
+
+def _generate_instances(base_seed: int, count: int, out_dir: Path, reveal_lead: float) -> list[Path]:
+ gen_mod = _load_host_module("generator") or __import__("generator")
+ paths: list[Path] = []
+ i = 0
+ attempts = 0
+ while len(paths) < count and attempts < count * 64:
+ inst = gen_mod.generate(base_seed * 1000 + i, GEN_DIFFS[i % len(GEN_DIFFS)], reveal_lead)
+ i += 1
+ attempts += 1
+ if not gen_mod._drivable(inst):
+ continue
+ p = out_dir / f"gen_{base_seed}_{len(paths) + 1}.json"
+ p.write_text(json.dumps(inst, ensure_ascii=False) + "\n", encoding="utf-8")
+ paths.append(p)
+ return paths
+
+
+def check_determinism(python: str, prog: Path, inst_path: Path, time_budget: float,
+ work: Path, reveal_lead: float | None = None) -> tuple[bool, str]:
+ """同一探针实例驱动两遍闭环,切段长度序列必须一致。"""
+ inst = load_instance(inst_path)
+ seqs: list[list[float]] = []
+ for _ in range(2):
+ client = AgentClient(prog, python, time_budget, work)
+ seqs.append(partition(inst, client.decide))
+ if seqs[0] != seqs[1]:
+ return False, "agent is not deterministic across two closed-loop runs"
+ return True, ""
+
+
+def _run_one(prog: Path, inst_path: Path, time_budget: float, py: str, tmp: Path) -> tuple[float, Any]:
+ inst = load_instance(inst_path)
+ client = AgentClient(prog, py, time_budget, tmp)
+ cuts = partition(inst, client.decide)
+ ok, m = score(inst, cuts)
+ if not ok:
+ return 0.0, {"status": "invalid", "reason": m.get("reason")}
+ return m["util"], {"status": "ok", "util": m["util"], "scrap": m["scrap"],
+ "n_cuts": len(cuts)}
+
+
+def evaluate(program_path: str, *, time_budget: float = 60.0, python: str | None = None,
+ data_dir: str | Path | None = None, reveal_lead: float | None = None) -> dict[str, Any]:
+ prog = Path(program_path).resolve()
+ if not prog.exists():
+ return {"combined_score": 0.0, "valid": 0.0, "per_instance": {}, "error": "program not found"}
+
+ violations = check_candidate(prog)
+
+ inst_dir = Path(data_dir).resolve() if data_dir else DATA_DIR
+ instances = sorted(inst_dir.glob("instance_*.json")) if inst_dir.is_dir() else []
+ if not instances:
+ # 若无固定实例集,现场生成一批(便于直接评测)
+ tmp = Path(tempfile.mkdtemp(prefix="oc_inst_"))
+ instances = _generate_instances(7, 6, tmp, reveal_lead or 10.0)
+
+ gen_seed_raw = os.environ.get("ONLINE_CUT_EVAL_GENERATE_SEED", "").strip()
+ tmp: Path | None = None
+ if gen_seed_raw:
+ base_seed = 0
+ try:
+ base_seed = int(gen_seed_raw)
+ except ValueError:
+ pass
+ count = DEFAULT_GENERATE_COUNT
+ try:
+ count = max(0, int(os.environ.get("ONLINE_CUT_EVAL_GENERATE_COUNT", "").strip()))
+ except ValueError:
+ pass
+ if count > 0:
+ tmp = Path(tempfile.mkdtemp(prefix="oc_gen_"))
+ instances = instances + _generate_instances(base_seed, count, tmp,
+ reveal_lead or 10.0)
+
+ if not instances:
+ return {"combined_score": 0.0, "valid": 0.0, "per_instance": {},
+ "error": "no instances"}
+
+ py = python or sys.executable
+
+ # 确定性探针:在一个探针实例上跑两遍闭合,切段序列必须一致
+ if not violations:
+ probe = instances[0]
+ work = Path(tempfile.mkdtemp(prefix="oc_probe_"))
+ det_ok, det_note = check_determinism(py, prog, probe, time_budget, work)
+ shutil.rmtree(work, ignore_errors=True)
+ if not det_ok:
+ violations = [f"determinism check failed: {det_note}"]
+
+ per_instance: dict[str, Any] = {}
+ total = 0.0
+ all_valid = True
+ work = Path(tempfile.mkdtemp(prefix="oc_run_"))
+ try:
+ for inst_path in instances:
+ if violations:
+ per_instance[inst_path.name] = {"status": "preflight_failed", "reasons": violations}
+ continue
+ util, info = _run_one(prog, inst_path, time_budget, py, work)
+ per_instance[inst_path.name] = info
+ if util <= 0 and info.get("status") != "ok":
+ all_valid = False
+ total += util
+ finally:
+ shutil.rmtree(work, ignore_errors=True)
+ if tmp is not None:
+ shutil.rmtree(tmp, ignore_errors=True)
+
+ combined = total / len(instances) if instances else 0.0
+ return {"combined_score": round(combined, 2), "valid": 1.0 if all_valid and not violations else 0.0,
+ "per_instance": per_instance, "num_instances": len(instances),
+ "time_budget_s": time_budget, "generate_seed": gen_seed_raw or None}
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description="在线切割评测")
+ parser.add_argument("solver")
+ parser.add_argument("--time-budget", type=float, default=60.0)
+ parser.add_argument("--data-dir", type=str, default=None)
+ parser.add_argument("--reveal-lead", type=float, default=None,
+ help="覆盖实例的 reveal_lead 显式指定(便于多配置对比)")
+ args = parser.parse_args(argv)
+ result = evaluate(args.solver, time_budget=args.time_budget,
+ data_dir=args.data_dir, reveal_lead=args.reveal_lead)
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py
new file mode 100644
index 00000000..72e484fd
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py
@@ -0,0 +1,100 @@
+"""在线切割实例生成器:种子生成隐藏异常表(不进 agent 可见实例)。
+
+参数语义:
+- 浇铸时长 T_cast 决定总材料长度 S = v*T_cast(流坐标 [0, S])。
+- 隐藏异常由 anomaly_seed 确定性生成(position、scrap_len 固定 0.8m),
+ 在 process.reveal_lead 确定的提前量下"逐步揭示"给 agent。
+- 实例 JSON 含 `defects`(隐藏表,仅供 simulator/参考解使用);
+ `strip_for_agent(inst)` 会去掉 defects/anomaly_seed,得到 agent 可见的版本。
+"""
+
+from __future__ import annotations
+
+import json
+import random
+import sys
+from pathlib import Path
+from typing import Any
+
+if __package__ in (None, ""):
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+MIN_BASIC = 4.8
+MAX_BASIC = 12.6
+LIMITS = {"min_basic": 4.8, "max_basic": 12.6, "min_process": 8.0, "max_process": 11.6}
+
+
+def generate(seed: int, difficulty: str = "medium", reveal_lead: float = 60.0,
+ n_anomaly: int | None = None) -> dict[str, Any]:
+ rng = random.Random(seed * 10007 + 11)
+ if difficulty == "easy":
+ s_lo, s_hi, a_lo, a_hi = 60.0, 90.0, 1, 3
+ elif difficulty == "hard":
+ s_lo, s_hi, a_lo, a_hi = 120.0, 180.0, 6, 12
+ else:
+ s_lo, s_hi, a_lo, a_hi = 80.0, 130.0, 3, 7
+
+ S = round(rng.uniform(s_lo, s_hi), 2)
+ n = n_anomaly if n_anomaly is not None else rng.randint(a_lo, a_hi)
+ T = float(rng.choice([8.5, 9.5, 11.1]))
+ t_min, t_max = T - 0.5, T + 0.5
+
+ scrap_len = 0.8
+ defects: list[list[float]] = []
+ # 生成异常位置:彼此与两端都留出 >= min_basic 的干净材料(保证每段可切、可污染)
+ low = MIN_BASIC
+ high = S - MIN_BASIC - scrap_len
+ attempts = 0
+ while len(defects) < n and attempts < 800:
+ attempts += 1
+ cand = round(rng.uniform(low, high), 2)
+ ok = True
+ for a, b in defects:
+ if cand < b + MIN_BASIC and a < cand + scrap_len + MIN_BASIC:
+ ok = False
+ break
+ if ok:
+ defects.append([cand, round(cand + scrap_len, 2)])
+ defects.sort()
+
+ # 每 1000 米诞生一个异常不现实——这里按"异常密度"换算为浇铸时长。
+ # T_cast 取 S/v + 充分裕量,确保整根材料都在观测范围内。
+ v = 1.0
+ t_cast = round(S / v + 120.0, 2)
+
+ inst: dict[str, Any] = {
+ "seed": seed,
+ "anomaly_seed": seed + 100000,
+ "process": {"v": v, "tc": 3, "tr": 1, "buffer_len": 60.0,
+ "scrap_len": scrap_len, "reveal_lead": reveal_lead},
+ "cast": {"total_length": S, "t_cast": t_cast},
+ "customer": {"target": T, "target_min": t_min, "target_max": t_max},
+ "limits": LIMITS,
+ "defects": defects,
+ }
+ return inst
+
+
+def strip_for_agent(inst: dict[str, Any]) -> dict[str, Any]:
+ """agent 可见版本:不含隐藏异常表/异常种子。"""
+ out = dict(inst)
+ out.pop("defects", None)
+ out.pop("anomaly_seed", None)
+ return out
+
+
+def _drivable(inst: dict[str, Any]) -> bool:
+ return bool(inst["defects"]) and float(inst["cast"]["total_length"]) > 30.0
+
+
+if __name__ == "__main__":
+ import sys as _s
+ if len(_s.argv) < 2:
+ print("usage: python generator.py [difficulty] [reveal_lead]", file=_s.stderr)
+ raise SystemExit(2)
+ out = Path(_s.argv[1])
+ diff = _s.argv[2] if len(_s.argv) > 2 else "medium"
+ rl = float(_s.argv[3]) if len(_s.argv) > 3 else 60.0
+ inst = generate(1, diff, rl)
+ out.write_text(json.dumps(inst, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
+ print(f"wrote {out} S={inst['cast']['total_length']} #defects={len(inst['defects'])} reveal_lead={rl}")
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
new file mode 100644
index 00000000..e6df657a
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
@@ -0,0 +1,87 @@
+"""多运行(多种子/多轮)agent 分数统计:对一组框架运行目录取 combined_score 的 mean±std。
+
+用法:
+ python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimization/openevolve
+ python verification/multiseed_stat.py --runs-dir "runs/**/openevolve/deepseek-v4-flash" # glob
+ python verification/multiseed_stat.py --runs-dir --pattern "*openevolve*"
+
+从每个运行目录下读 `/best/best_program_info.json` 的 `metrics.combined_score`
+(框架统一保存的 best 程序分数),对多次运行做 mean / std / min / max,
+并给出"相对 reference 的余量"(gap = reference_util - mean)。
+
+对齐 CVRP 评审点:agent 分数需多轮均值±std(而非单次),衡量稳定性与真实水平。
+纯标准库。
+"""
+
+from __future__ import annotations
+
+import argparse
+import glob
+import json
+import math
+import statistics
+from pathlib import Path
+
+DEFAULT_REFERENCE = 88.72 # 全知参考解利用率(verification/ref_solver.py),可 --reference 覆盖
+
+
+def _best_score(run_dir: Path) -> float | None:
+ for rel in ("openevolve/best/best_program_info.json",
+ "shinkaevolve/best/best_program_info.json",
+ "abmcts/best/best_program_info.json",
+ "best/best_program_info.json"):
+ p = run_dir / rel
+ if p.is_file():
+ try:
+ return float(json.loads(p.read_text(encoding="utf-8"))["metrics"]["combined_score"])
+ except Exception:
+ return None
+ return None
+
+
+def collect(runs_dir: str | Path, pattern: str | None = None) -> list[tuple[Path, float]]:
+ base = Path(runs_dir)
+ if pattern:
+ bests = sorted(Path(p) for p in glob.glob(str(base / pattern), recursive=True))
+ else:
+ bests = sorted(base.rglob("best/best_program_info.json"))
+ out: list[tuple[Path, float]] = []
+ seen: set[Path] = set()
+ for best in bests:
+ # best_program_info.json 位于 //best/ 下
+ run_dir = best.parent.parent.parent
+ if run_dir in seen:
+ continue
+ score = _best_score(run_dir)
+ if score is not None:
+ out.append((run_dir, score))
+ seen.add(run_dir)
+ return out
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="多运行 agent 分数统计")
+ parser.add_argument("--runs-dir", required=True, help="运行目录或 glob 模式")
+ parser.add_argument("--pattern", default=None, help="追加的子 glob 模式")
+ parser.add_argument("--reference", type=float, default=DEFAULT_REFERENCE,
+ help="参考解利用率(默认 88.72)")
+ args = parser.parse_args()
+
+ pairs = collect(args.runs_dir, args.pattern)
+ if not pairs:
+ print("no runs found under", args.runs_dir)
+ return 1
+
+ scores = [s for _, s in pairs]
+ mean = statistics.mean(scores)
+ std = statistics.stdev(scores) if len(scores) > 1 else 0.0
+ print(f"runs: {len(scores)}")
+ for run_dir, s in pairs:
+ print(f" {run_dir.name}: {s}")
+ print(f"mean={mean:.2f} std={std:.2f} min={min(scores):.2f} max={max(scores):.2f}")
+ print(f"gap to reference ({args.reference}): {args.reference - mean:.2f}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/ref_solver.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/ref_solver.py
new file mode 100644
index 00000000..05f71953
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/ref_solver.py
@@ -0,0 +1,109 @@
+"""在线切割全知参考解(clairvoyant,看到全部报废表)。
+
+静态一维划分 DP(网格化,纯标准库):
+- 流坐标 [0,S],网格 GRID;每块长度 ∈ [min_basic, max_basic]。
+- 一块若与任一报废段区间重叠 -> 整块报废(cost = 块长,无贴合度惩罚);
+ 否则按干净块规则( bool:
+ for xa, xb in defects:
+ if x1 > xa + OVERLAP_TOL and x0 < xb - OVERLAP_TOL:
+ return True
+ return False
+
+
+def _cost(inst: dict[str, Any], x0: float, c: float, defects: list[list[float]]) -> float:
+ if _piece_overlap(x0, x0 + c, defects):
+ return c # 污染块,整块报废(无贴合度惩罚)
+ t = float(inst["customer"]["target"])
+ t_max = float(inst["customer"]["target_max"])
+ lim_min_proc = _limits(inst)[2]
+ if c < lim_min_proc:
+ return c
+ scrap = max(0.0, c - t_max)
+ penalty = abs(min(c, t_max) - t)
+ return scrap + LAMBDA * penalty
+
+
+def solve(inst: dict[str, Any]) -> dict[str, Any]:
+ """返回 {'cuts': [...]}:全知最优切段。"""
+ import math
+
+ S = float(inst["cast"]["total_length"])
+ defects = [list((a, b)) for a, b in inst.get("defects", []) or []]
+ min_basic, max_basic, _ = _limits(inst)
+ n = max(1, int(round(S / GRID)))
+ min_steps = int(round(min_basic / GRID))
+ max_steps = int(round(max_basic / GRID))
+
+ INF = float("inf")
+ f = [INF] * (n + 1)
+ bp = [-1] * (n + 1)
+ f[0] = 0.0
+ for pos in range(min_steps, n + 1):
+ best = INF
+ best_s = -1
+ hi = min(max_steps, pos)
+ for s in range(min_steps, hi + 1):
+ prev = pos - s
+ if f[prev] >= INF:
+ continue
+ val = f[prev] + _cost(inst, prev * GRID, s * GRID, defects)
+ if val < best:
+ best = val
+ best_s = s
+ f[pos] = best
+ bp[pos] = best_s
+
+ if f[n] >= INF:
+ # 兜底:均匀等分(保证产出合法切段)
+ k = max(1, math.ceil(S / max_basic))
+ base = S / k
+ cuts = [round(base, 4)] * k
+ return {"cuts": cuts}
+
+ cuts: list[float] = []
+ pos = n
+ while pos > 0:
+ s = bp[pos]
+ cuts.append(round(s * GRID, 4))
+ pos -= s
+ cuts.reverse()
+ # 消除浮点累计误差,使求和精确等于 S
+ drift = S - sum(cuts)
+ cuts[-1] = round(cuts[-1] + drift, 4)
+ return {"cuts": cuts}
+
+
+def best_util(inst: dict[str, Any]) -> float:
+ """全知参考解的材料利用率。"""
+ cuts = solve(inst)["cuts"]
+ ok, m = score(inst, cuts)
+ return m.get("util", 0.0) if ok else 0.0
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/requirements.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/requirements.txt
new file mode 100644
index 00000000..4e66700e
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/requirements.txt
@@ -0,0 +1,2 @@
+# The evaluator uses only the Python standard library.
+# Runtime requirement: Python >= 3.10 (no third-party dependencies).
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
new file mode 100644
index 00000000..b204a48a
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
@@ -0,0 +1,154 @@
+"""连铸切割在线模拟器(闭环、最小切段污染、揭示提前量)。
+
+模型(详见 Task.md):
+- 材料用流坐标 x∈[0,S],S = v*浇铸时长。横截面 x 到达切割点在 x/v + D/v。
+- 隐藏异常(由 anomaly_seed 决定):报废段占流区间 [x_a, x_a+scrap_len]。
+- 揭示提前量 reveal_lead(分钟;v=1 时等于"上游米数"):一段报废料在
+ x_a <= cut_pos + reveal_lead 时"揭示"给 agent;否则不可见。reveal_lead=60 为忠实版
+ (τ_a 即知,60m 提前量);reveal_lead= min_basic 的
+ 污染块里、整块报废(不允许像离线版那样把报废段当 0.8m 小块切出)。
+- 评分:scrap = 所有污染块长度 + 干净块超窗口报废 + 干净块 dict[str, Any]:
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+def _p(inst: dict[str, Any], key: str):
+ proc = inst.get("process", {})
+ return float(proc.get(key, DEFAULTS[key]))
+
+
+def _limits(inst: dict[str, Any]) -> dict[str, float]:
+ lim = inst.get("limits", {})
+ return {
+ "min_basic": float(lim.get("min_basic", DEFAULTS["min_basic"])),
+ "max_basic": float(lim.get("max_basic", DEFAULTS["max_basic"])),
+ "min_process": float(lim.get("min_process", DEFAULTS["min_process"])),
+ "max_process": float(lim.get("max_process", DEFAULTS["max_process"])),
+ }
+
+
+def visible_defects(inst: dict[str, Any], cut_pos: float) -> list[dict[str, float]]:
+ """返回 `cut_pos` 处 agent 可见的报废段(已揭示且未完全过去)。"""
+ reveal = _p(inst, "reveal_lead")
+ out: list[dict[str, float]] = []
+ for xa, xb in inst.get("defects", []) or []:
+ if xa <= cut_pos + reveal + OVERLAP_TOL and xb > cut_pos + OVERLAP_TOL:
+ out.append({"x": xa, "x_end": xb})
+ return out
+
+
+def _make_state(inst: dict[str, Any], cut_pos: float, committed: list[float]) -> dict[str, Any]:
+ cust = inst.get("customer", {})
+ lim = _limits(inst)
+ return {
+ "cut_pos": round(cut_pos, 4),
+ "committed": committed,
+ "visible_defects": sorted(visible_defects(inst, cut_pos), key=lambda d: d["x"]),
+ "total_length": float(inst["cast"]["total_length"]),
+ "target": float(cust.get("target", 9.5)),
+ "target_min": float(cust.get("target_min", 9.0)),
+ "target_max": float(cust.get("target_max", 10.0)),
+ "limits": {
+ "min_basic": lim["min_basic"], "max_basic": lim["max_basic"],
+ "min_process": lim["min_process"], "max_process": lim["max_process"],
+ },
+ "reveal_lead": _p(inst, "reveal_lead"),
+ }
+
+
+def partition(inst: dict[str, Any], decision_fn: Callable[[dict[str, Any]], float]) -> list[float]:
+ """闭环推进:反复喂状态、取下一刀,直到切完;返回切段长度列表(之和 = S)。"""
+ S = float(inst["cast"]["total_length"])
+ lim = _limits(inst)
+ committed: list[float] = []
+ cut_pos = 0.0
+ guard = 0
+ while S - cut_pos > SUM_TOL and guard < 100000:
+ guard += 1
+ remaining = S - cut_pos
+ if remaining <= lim["max_basic"] + SUM_TOL:
+ # 最后一段 = 剩余材料(agent 不决定,直接收尾)
+ piece = remaining
+ else:
+ L = decision_fn(_make_state(inst, cut_pos, list(committed)))
+ # 校验可接受:非法/越界则回退到目标值夹取(保证推进)
+ if not isinstance(L, (int, float)) or math.isnan(L) or math.isinf(L):
+ L = cust_target(inst)
+ L = float(L)
+ piece = max(lim["min_basic"], min(lim["max_basic"], L))
+ if piece > remaining:
+ piece = remaining
+ committed.append(round(piece, 4))
+ cut_pos += piece
+ return committed
+
+
+def cust_target(inst: dict[str, Any]) -> float:
+ return float(inst.get("customer", {}).get("target", 9.5))
+
+
+def _piece_is_clean(piece_iv: tuple[float, float], defects: list[list[float]]) -> bool:
+ (x0, x1) = piece_iv
+ for xa, xb in defects:
+ if x1 > xa + OVERLAP_TOL and x0 < xb - OVERLAP_TOL:
+ return False # 与任一报废段重叠 -> 污染
+ return True
+
+
+def score(inst: dict[str, Any], cuts: list[float]) -> tuple[bool, dict[str, Any]]:
+ """给定整根切段,校验 + 评分。返回 (valid, metrics)。"""
+ S = float(inst["cast"]["total_length"])
+ lim = _limits(inst)
+ if abs(sum(cuts) - S) > SUM_TOL:
+ return False, {"valid": False, "reason": f"sum {sum(cuts)} != S {S}"}
+ # 每块不得超过 [max_basic];< min_basic 视为报废(尾段/短块物理上无法运走,作废)
+ for c in cuts:
+ if c > lim["max_basic"] + SUM_TOL:
+ return False, {"valid": False, "reason": f"cut {c} exceeds max_basic"}
+
+ defects = [list((a, b)) for a, b in inst.get("defects", []) or []]
+ boundaries = [0.0]
+ for c in cuts:
+ boundaries.append(boundaries[-1] + c)
+ total_scrap = 0.0
+ total_penalty = 0.0
+ for i, c in enumerate(cuts):
+ if c < lim["min_basic"] - SUM_TOL:
+ total_scrap += c # 短块/尾段:物理无法运走,整块报废
+ continue
+ iv = (boundaries[i], boundaries[i + 1])
+ if not _piece_is_clean(iv, defects):
+ total_scrap += c # 污染块整块报废
+ continue
+ # 干净块
+ if c < lim["min_process"]:
+ total_scrap += c # <8.0 -> 整块报废
+ else:
+ total_scrap += max(0.0, c - float(inst["customer"]["target_max"]))
+ total_penalty += abs(min(c, float(inst["customer"]["target_max"]))
+ - float(inst["customer"]["target"]))
+ util = max(0.0, 100.0 * (S - total_scrap) / S)
+ return True, {"valid": True, "scrap": round(total_scrap, 4),
+ "penalty": round(total_penalty, 4), "util": round(util, 2),
+ "cuts": cuts}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_evaluator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_evaluator.py
new file mode 100644
index 00000000..d45afa3e
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_evaluator.py
@@ -0,0 +1,63 @@
+"""online evaluate.py 单测:闭环驱动、作弊拒绝、运行时生成。"""
+
+from __future__ import annotations
+
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import evaluate # noqa: E402
+
+BASE = Path(__file__).resolve().parent.parent / "baseline" / "solver.py"
+DATA = Path(__file__).resolve().parent / "data" / "instances"
+
+CHEAT = '''#!/usr/bin/env python3
+from __future__ import annotations
+import json, sys
+
+def decide(state):
+ # EVOLVE-BLOCK-START
+ from ref_solver import solve, best_util
+ return float(state["target"])
+ # EVOLVE-BLOCK-END
+
+def main():
+ state = json.load(open(sys.argv[1]))
+ print(json.dumps({"piece_length": decide(state)}))
+
+if __name__ == "__main__":
+ main()
+'''
+
+
+class TestEvaluator(unittest.TestCase):
+ def test_baseline_valid(self):
+ res = evaluate.evaluate(str(BASE), time_budget=30, data_dir=str(DATA))
+ self.assertEqual(res["valid"], 1.0)
+ self.assertGreater(res["combined_score"], 0.0)
+
+ def test_cheat_rejected(self):
+ with tempfile.TemporaryDirectory() as td:
+ c = Path(td) / "cheat.py"
+ c.write_text(CHEAT, encoding="utf-8")
+ res = evaluate.evaluate(str(c), time_budget=30, data_dir=str(DATA))
+ self.assertEqual(res["valid"], 0.0)
+
+ def test_runtime_generation(self):
+ import os
+ os.environ["ONLINE_CUT_EVAL_GENERATE_SEED"] = "7"
+ os.environ["ONLINE_CUT_EVAL_GENERATE_COUNT"] = "3"
+ try:
+ res = evaluate.evaluate(str(BASE), time_budget=30, data_dir=str(DATA))
+ finally:
+ os.environ.pop("ONLINE_CUT_EVAL_GENERATE_SEED", None)
+ os.environ.pop("ONLINE_CUT_EVAL_GENERATE_COUNT", None)
+ gen = [k for k in res["per_instance"] if k.startswith("gen_")]
+ self.assertEqual(len(gen), 3)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_frontier_eval_evaluator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_frontier_eval_evaluator.py
new file mode 100644
index 00000000..6a0e86a5
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_frontier_eval_evaluator.py
@@ -0,0 +1,65 @@
+"""online frontier_eval/evaluator.py(沙箱入口)测试:与 verification 版一致、拒作弊。"""
+
+from __future__ import annotations
+
+import importlib.util
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+TASK = Path(__file__).resolve().parent.parent
+
+
+def _load_evaluator():
+ p = TASK / "frontier_eval" / "evaluator.py"
+ spec = importlib.util.spec_from_file_location("_oc_sandbox_evaluator", p)
+ m = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(m)
+ return m
+
+
+def _cheat(d: Path) -> Path:
+ p = d / "cheat.py"
+ p.write_text(
+ '#!/usr/bin/env python3\nfrom __future__ import annotations\nimport json, sys\n'
+ 'def decide(state):\n # EVOLVE-BLOCK-START\n from ref_solver import solve\n'
+ ' return float(state["target"])\n # EVOLVE-BLOCK-END\n'
+ 'def main():\n state=json.load(open(sys.argv[1]))\n'
+ ' print(json.dumps({"piece_length": decide(state)}))\n'
+ 'if __name__=="__main__": main()\n',
+ encoding="utf-8",
+ )
+ return p
+
+
+class TestSandboxEvaluator(unittest.TestCase):
+ def setUp(self):
+ sys.path.insert(0, str(TASK / "verification"))
+ self.ev = _load_evaluator()
+ self.data = str(TASK / "verification" / "data" / "instances")
+
+ def test_baseline_valid(self):
+ import os
+ os.environ.pop("ONLINE_CUT_EVAL_GENERATE_SEED", None)
+ r = self.ev.evaluate(str(TASK / "baseline" / "solver.py"), data_dir=self.data)
+ r = r if isinstance(r, dict) and "combined_score" in r else r.get("metrics", r)
+ self.assertEqual(r.get("valid"), 1.0)
+ self.assertGreater(r.get("combined_score", 0.0), 0.0)
+
+ def test_cheat_rejected(self):
+ with tempfile.TemporaryDirectory() as td:
+ r = self.ev.evaluate(str(_cheat(Path(td))), data_dir=self.data)
+ r = r if isinstance(r, dict) and "combined_score" in r else r.get("metrics", r)
+ self.assertEqual(r.get("valid"), 0.0)
+
+ def test_consistent_with_verification(self):
+ from evaluate import evaluate as ver
+ r = self.ev.evaluate(str(TASK / "baseline" / "solver.py"), data_dir=self.data)
+ r = r if isinstance(r, dict) and "combined_score" in r else r.get("metrics", r)
+ v = ver(str(TASK / "baseline" / "solver.py"), data_dir=self.data)
+ self.assertAlmostEqual(r["combined_score"], v["combined_score"], places=2)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_generator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_generator.py
new file mode 100644
index 00000000..65bc4736
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_generator.py
@@ -0,0 +1,41 @@
+"""online generator.py 单测:确定性、隐藏异常、可驱动性。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import generator # noqa: E402
+
+
+class TestGenerate(unittest.TestCase):
+ def test_deterministic(self):
+ self.assertEqual(generator.generate(7, "medium", 10), generator.generate(7, "medium", 10))
+
+ def test_reveal_lead_set(self):
+ for rl in (10.0, 60.0):
+ inst = generator.generate(1, "medium", rl)
+ self.assertEqual(inst["process"]["reveal_lead"], rl)
+
+ def test_defects_within_bounds_and_sorted(self):
+ inst = generator.generate(6, "hard", 10)
+ S = float(inst["cast"]["total_length"])
+ prev = 0.0
+ for a, b in inst["defects"]:
+ self.assertGreater(a, prev)
+ self.assertGreaterEqual(a, inst["limits"]["min_basic"] - 1e-6)
+ self.assertLessEqual(b, S)
+ prev = b
+
+ def test_strip_for_agent_hides_defects(self):
+ inst = generator.generate(3, "medium", 10)
+ ag = generator.strip_for_agent(inst)
+ self.assertNotIn("defects", ag)
+ self.assertNotIn("anomaly_seed", ag)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_ref_solver.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_ref_solver.py
new file mode 100644
index 00000000..c54d6b4e
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_ref_solver.py
@@ -0,0 +1,42 @@
+"""online ref_solver.py 单测:全知参考解合法性、确定性、优于朴素基线。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import ref_solver # noqa: E402
+from simulator import partition, score # noqa: E402
+import generator # noqa: E402
+
+
+class TestRefSolver(unittest.TestCase):
+ def test_solution_valid_and_sums(self):
+ for seed in (1, 2, 3):
+ inst = generator.generate(seed, "medium", 10)
+ cuts = ref_solver.solve(inst)["cuts"]
+ ok, m = score(inst, cuts)
+ self.assertTrue(ok)
+ self.assertAlmostEqual(sum(cuts), float(inst["cast"]["total_length"]), places=2)
+
+ def test_deterministic(self):
+ inst = generator.generate(1, "hard", 10)
+ self.assertEqual(ref_solver.solve(inst), ref_solver.solve(inst))
+
+ def test_beats_naive_baseline(self):
+ """全知参考解的利用率应 > 朴素"恒切目标值"基线。"""
+ def naive(state):
+ return float(state["target"])
+ for seed in (1, 2):
+ inst = generator.generate(seed, "medium", 10)
+ ref_u = ref_solver.best_util(inst)
+ ok, m = score(inst, partition(inst, naive))
+ base_u = m["util"] if ok else 0.0
+ self.assertGreater(ref_u, base_u, f"seed {seed}")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_simulator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_simulator.py
new file mode 100644
index 00000000..c56427ed
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_simulator.py
@@ -0,0 +1,67 @@
+"""online simulator.py 单测:评分合法性与校验规则。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import generator # noqa: E402
+import ref_solver # noqa: E402
+from simulator import partition, score # noqa: E402
+
+
+def _inst(seed: int = 1, reveal: float = 10.0, diff: str = "medium"):
+ return generator.generate(seed, diff, reveal)
+
+
+def _decide(state):
+ return float(state["target"])
+
+
+class TestScore(unittest.TestCase):
+ def test_ref_solution_scores_valid(self):
+ for seed in (1, 2, 3):
+ inst = _inst(seed)
+ cuts = ref_solver.solve(inst)["cuts"]
+ ok, m = score(inst, cuts)
+ self.assertTrue(ok, f"seed {seed}")
+ self.assertGreaterEqual(m["scrap"], 0.0)
+ self.assertGreaterEqual(m["util"], 0.0)
+ self.assertLessEqual(m["util"], 100.0)
+
+ def test_short_tail_scrapped_not_invalid(self):
+ # 手搓小实例:最后一段 <4.8 视为报废,不判非法
+ inst = {"seed": 0,
+ "process": {"v": 1.0, "tc": 3, "tr": 1, "buffer_len": 60.0,
+ "scrap_len": 0.8, "reveal_lead": 10.0},
+ "cast": {"total_length": 20.0, "t_cast": 140.0},
+ "customer": {"target": 9.0, "target_min": 8.5, "target_max": 9.5},
+ "limits": {"min_basic": 4.8, "max_basic": 12.6,
+ "min_process": 8.0, "max_process": 11.6},
+ "defects": []}
+ cuts = [8.0, 8.0, 4.0] # 尾段 4.0 < 4.8
+ ok, m = score(inst, cuts)
+ self.assertTrue(ok)
+ # 尾段 4.0 报废;8.0 在 [min_process, target_min) 内 -> 0 报废
+ self.assertAlmostEqual(m["scrap"], 4.0, places=2)
+
+
+class TestPartition(unittest.TestCase):
+ def test_partition_sums_to_total(self):
+ inst = _inst(2, 10)
+ cuts = partition(inst, _decide)
+ self.assertAlmostEqual(sum(cuts), float(inst["cast"]["total_length"]), places=2)
+
+ def test_partition_intermediate_in_window(self):
+ inst = _inst(3, 10)
+ cuts = partition(inst, _decide)
+ lo = inst["limits"]["min_basic"]
+ for c in cuts[:-1]:
+ self.assertGreaterEqual(c, lo - 1e-3)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_validator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_validator.py
new file mode 100644
index 00000000..3c2f9240
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/test_validator.py
@@ -0,0 +1,73 @@
+"""online validator.py 单测:静态检查、禁引用、绝对路径、硬编码、环境剥离。"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+from validator import candidate_env, static_check_source # noqa: E402
+
+BASE = Path(__file__).resolve().parent.parent / "baseline" / "solver.py"
+
+TPL = '''#!/usr/bin/env python3
+from __future__ import annotations
+import json, sys
+
+def decide(state):
+ # EVOLVE-BLOCK-START
+ return float(state["target"])
+ # EVOLVE-BLOCK-END
+
+def main():
+ import json as _j
+ state = _j.load(open(sys.argv[1]))
+ print(_j.dumps({"piece_length": decide(state)}))
+
+if __name__ == "__main__":
+ main()
+'''
+
+
+class TestValidator(unittest.TestCase):
+ def test_baseline_passes(self):
+ self.assertEqual(_check(BASE, BASE), [])
+
+ def test_ref_import_caught(self):
+ src = TPL.replace('return float(state["target"])',
+ 'from ref_solver import solve\n return 0.0')
+ self.assertTrue(any("ref_solver" in m for m in static_check_source(src)))
+
+ def test_generator_import_caught(self):
+ src = TPL.replace('return float(state["target"])', 'import generator\n return 0.0')
+ self.assertTrue(any("generator" in m.lower() for m in static_check_source(src)))
+
+ def test_absolute_path_caught(self):
+ src = TPL.replace('return float(state["target"])', 'open("C:\\\\Users\\\\x")')
+ self.assertTrue(any("absolute" in m for m in static_check_source(src)))
+
+ def test_missing_markers_caught(self):
+ src = TPL.replace("# EVOLVE-BLOCK-START\n ", "").replace("# EVOLVE-BLOCK-END\n", "")
+ self.assertTrue(any("EVOLVE-BLOCK" in m for m in static_check_source(src)))
+
+ def test_env_strips_frontier(self):
+ import os
+ os.environ["ONLINE_CUT_EVAL_GENERATE_SEED"] = "42"
+ os.environ["FRONTIER_EVAL_SOMETHING"] = "x"
+ try:
+ env = candidate_env()
+ self.assertNotIn("ONLINE_CUT_EVAL_GENERATE_SEED", env)
+ self.assertNotIn("FRONTIER_EVAL_SOMETHING", env)
+ finally:
+ os.environ.pop("ONLINE_CUT_EVAL_GENERATE_SEED", None)
+ os.environ.pop("FRONTIER_EVAL_SOMETHING", None)
+
+
+def _check(p, base):
+ return static_check_source(p.read_text(encoding="utf-8"), base.read_text(encoding="utf-8"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/validator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/validator.py
new file mode 100644
index 00000000..31a7d778
--- /dev/null
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/validator.py
@@ -0,0 +1,93 @@
+"""在线切割候选完整性校验(静态检查 + 环境剥离,借鉴离线/CVRP 惯例)。
+
+评分前以可执行检查强制约束:
+ 1. EVOLVE-BLOCK 完整性 + 标记外代码与初始 baseline 逐字节一致;
+ 2. 禁引用评测/生成/参考解模块(evaluate/generator/ref_solver)、绝对路径、按实例名硬编码;
+ 3. candidate_env:候选子进程剥离 FRONTIER_*/ONLINE_CUT_EVAL_* 变量,封宿主侧信道。
+ 4. 确定性探针(闭环跑两遍)在 evaluate.py 中执行。
+
+候选允许 import verification/simulator.py(白盒计分器),但禁止评测/生成/参考解逻辑。
+"""
+
+from __future__ import annotations
+
+import os
+import re
+from pathlib import Path
+
+EVOLVE_START = "EVOLVE-BLOCK-START"
+EVOLVE_END = "EVOLVE-BLOCK-END"
+PROJECT_PREFIX = "ONLINE_CUT_EVAL_"
+
+STRONG_TOKENS = (
+ "ref_solver",
+ "ONLINE_CUT_EVAL_",
+ "from generator",
+ "import generator",
+ "anomaly_seed",
+)
+FORBIDDEN_RE = (
+ re.compile(r"verification[\\/](?:evaluate|generator|ref_solver)"),
+ re.compile(r"verification\s*\.\s*(?:evaluate|generator|ref_solver)\b"),
+ re.compile(r"\b(?:from|import)\s+(?:evaluate|generator|ref_solver)\b"),
+)
+ABS_PATH_RE = re.compile(r"[A-Za-z]:[\\/]|/home/|/Users/")
+HARDCODE_RE = re.compile(r"[\"'](?:instance|gen)_\d+(?:_\d+)?[\"']\s*:")
+
+
+def split_evolve_blocks(src: str) -> tuple[str, str, str] | None:
+ start = src.find(EVOLVE_START)
+ end = src.find(EVOLVE_END)
+ if start == -1 or end == -1 or end <= start:
+ return None
+ return (src[:start], src[start + len(EVOLVE_START):end], src[end + len(EVOLVE_END):])
+
+
+def fixed_region(parts: tuple[str, str, str]) -> str:
+ return (parts[0] + parts[2]).replace("\r\n", "\n").rstrip("\n")
+
+
+def static_check_source(src: str, baseline_src: str | None = None) -> list[str]:
+ issues: list[str] = []
+ parts = split_evolve_blocks(src)
+ if parts is None:
+ issues.append("missing EVOLVE-BLOCK-START / EVOLVE-BLOCK-END markers")
+ elif baseline_src is not None:
+ init_parts = split_evolve_blocks(baseline_src)
+ if init_parts is not None and fixed_region(init_parts) != fixed_region(parts):
+ issues.append("code outside EVOLVE-BLOCK differs from initial baseline")
+ for token in STRONG_TOKENS:
+ if token in src:
+ issues.append(f"candidate references forbidden token {token!r}")
+ for pat in FORBIDDEN_RE:
+ if pat.search(src):
+ issues.append("candidate references forbidden evaluation/generation/ref module")
+ if ABS_PATH_RE.search(src):
+ issues.append("candidate contains an absolute filesystem path")
+ if HARDCODE_RE.search(src):
+ issues.append("candidate hardcodes per-instance schedules by name")
+ return issues
+
+
+def check_candidate(solver_path: Path | str, baseline_path: Path | str | None = None) -> list[str]:
+ solver_path = Path(solver_path)
+ try:
+ src = solver_path.read_text(encoding="utf-8", errors="replace")
+ except Exception as exc:
+ return [f"cannot read candidate source: {exc}"]
+ baseline_src = None
+ if baseline_path is not None:
+ try:
+ baseline_src = Path(baseline_path).read_text(encoding="utf-8", errors="replace")
+ except Exception:
+ baseline_src = None
+ return static_check_source(src, baseline_src)
+
+
+def candidate_env() -> dict[str, str]:
+ env = os.environ.copy()
+ for key in list(env):
+ upper = key.upper()
+ if upper.startswith("FRONTIER") or upper.startswith(PROJECT_PREFIX):
+ del env[key]
+ return env
diff --git a/benchmarks/ContinuousCasting/README.md b/benchmarks/ContinuousCasting/README.md
new file mode 100644
index 00000000..a8b4e6d3
--- /dev/null
+++ b/benchmarks/ContinuousCasting/README.md
@@ -0,0 +1,23 @@
+# ContinuousCasting
+
+This domain collects deterministic engineering-optimization tasks for continuous steel casting
+(连铸) and the adjacent industrial process-control problems. Current tasks emphasize realistic
+operational constraints, a well-defined material-utilization objective, and executable,
+standard-library-only verification.
+
+## Tasks
+
+- `CuttingOptimization`
+ - Unified benchmark: `task=unified task.benchmark=ContinuousCasting/CuttingOptimization`
+ - Quick run: `python -m frontier_eval task=unified task.benchmark=ContinuousCasting/CuttingOptimization algorithm.iterations=0`
+ - Description: cut a continuously cast steel billet (with fixed 0.8 m scrap segments) into
+ pieces to minimize total scrapped length, then make every shipped piece as close as possible
+ to a customer target length — inspired by the CUMCM 2021 Problem D. The optimum is reachable
+ (a strong agent can derive the exact partition DP).
+- `CuttingOptimizationOnline`
+ - Unified benchmark: `task=unified task.benchmark=ContinuousCasting/CuttingOptimizationOnline`
+ - Quick run: `python -m frontier_eval task=unified task.benchmark=ContinuousCasting/CuttingOptimizationOnline algorithm.iterations=0`
+ - Description: the **online (closed-loop)** version — the agent is told about a 0.8 m scrap
+ segment only when it is within `reveal_lead` m of the cut line, decides each cut with only the
+ visible past, and is scored against the full hidden defect set. An online agent cannot reach
+ the clairvoyant optimum (info asymmetry), so it is genuinely harder than the offline task.
diff --git a/benchmarks/ContinuousCasting/README_zh-CN.md b/benchmarks/ContinuousCasting/README_zh-CN.md
new file mode 100644
index 00000000..54585901
--- /dev/null
+++ b/benchmarks/ContinuousCasting/README_zh-CN.md
@@ -0,0 +1,19 @@
+# ContinuousCasting
+
+本 domain 汇集"连铸(连续浇铸钢材)及其相邻工业过程控制"的**确定性工程优化**任务。
+当前任务强调真实工况约束、明确的材料利用率目标,以及可执行、纯标准库的验证。
+
+## 任务
+
+- `CuttingOptimization`
+ - Unified 基准:`task=unified task.benchmark=ContinuousCasting/CuttingOptimization`
+ - 快速运行:`python -m frontier_eval task=unified task.benchmark=ContinuousCasting/CuttingOptimization algorithm.iterations=0`
+ - 描述:把一根连续浇铸的钢坯(带固定的 0.8 m 零废段)切成成品段,先最小化报废总长度,
+ 再让每块成品尽量贴近客户目标值——灵感来自 2021 全国大学生数学建模竞赛 D 题。最优可达
+ (强 agent 能推导出精确划分 DP)。
+- `CuttingOptimizationOnline`
+ - Unified 基准:`task=unified task.benchmark=ContinuousCasting/CuttingOptimizationOnline`
+ - 快速运行:`python -m frontier_eval task=unified task.benchmark=ContinuousCasting/CuttingOptimizationOnline algorithm.iterations=0`
+ - 描述:**在线(闭环)版**——agent 只在报废段距切割线 `reveal_lead` 米以内时才得知其存在,
+ 每个决策点只拿可见过去,最后按完整隐藏报废表评分。在线 agent 无法达到全知最优(信息不对称),
+ 因此比离线版更难。
From 6c00d95afd841579d528923fa842d86e0fbb40df Mon Sep 17 00:00:00 2001
From: zzy <17092805+mz2007@user.noreply.gitee.com>
Date: Sat, 5 Sep 2026 17:24:07 +0800
Subject: [PATCH 2/6] fix: resolve doc/vs-impl inconsistencies + online
anti-cheat hole
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Address reviewer feedback on the new ContinuousCasting benchmarks:
- online (threat model, high): fixed instances embedding the hidden defect schedule are no
longer copied into the sandbox (copy_files.txt drops verification/data/instances); the
evaluator loads them from the host source benchmark dir, so a candidate cannot read the
hidden defects out of a data file.
- online: unify naming/default to reveal_lead = 10.0 (generator default 60.0 -> 10.0; docs
reveal_dist/8.0 -> reveal_lead/10.0).
- online: scoring now applies the 1e-4 target-fit penalty in the utilization (matches the
offline formula; the docs already claimed it), so equal-scrap plans are tie-broken.
- online: Task.md interface section now matches the per-decision-call implementation (was
stale REPL text with wrong state keys).
- offline: titles disambiguate as offline/static ('连铸切割优化(离线/静态版)') so they are
not confused with the online task.
- online: remove a duplicated/stale README note; soften the 'cannot reach' claim to an
empirical observation + note the optional online-oracle reference layer.
---
.../CuttingOptimization/README.md | 2 +-
.../CuttingOptimization/README_zh-CN.md | 2 +-
.../CuttingOptimization/Task.md | 2 +-
.../CuttingOptimizationOnline/README.md | 15 +++++++++--
.../CuttingOptimizationOnline/README_zh-CN.md | 7 ++---
.../CuttingOptimizationOnline/Task.md | 27 ++++++++++---------
.../frontier_eval/copy_files.txt | 1 -
.../verification/evaluate.py | 19 +++++++++++--
.../verification/generator.py | 2 +-
.../verification/simulator.py | 4 ++-
10 files changed, 55 insertions(+), 26 deletions(-)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/README.md b/benchmarks/ContinuousCasting/CuttingOptimization/README.md
index 84d631f1..a540988f 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimization/README.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/README.md
@@ -1,4 +1,4 @@
-# CuttingOptimization: Online Optimization of Continuous-Casting Cutting (Frontier-Eng Benchmark)
+# CuttingOptimization: Continuous-Casting Cutting Optimization — offline/static (Frontier-Eng Benchmark)
An **original** Frontier-Engineering benchmark inspired by the CUMCM 2021 Problem D
(«连铸切割的在线优化»), formalized into a self-contained, deterministic optimization task.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md b/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
index 1ae3af2b..86f4b89b 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
@@ -1,4 +1,4 @@
-# CuttingOptimization:连铸切割的在线优化(Frontier-Eng 基准)
+# CuttingOptimization:连铸切割优化(离线/静态版,Frontier-Eng 基准)
一个**原创**的 Frontier-Engineering 基准,灵感来自 2021 全国大学生数学建模竞赛 D 题
《连铸切割的在线优化》,并被形式化成**确定性、自包含**的切割优化任务。
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/Task.md b/benchmarks/ContinuousCasting/CuttingOptimization/Task.md
index 7192e868..bb0670ba 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimization/Task.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/Task.md
@@ -1,4 +1,4 @@
-# 连铸切割的在线优化(CuttingOptimization)
+# 连铸切割优化(离线/静态版,CuttingOptimization)
## 1. 背景
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
index 6fb4d612..060e2d0a 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
@@ -6,8 +6,8 @@ An **original** Frontier-Engineering benchmark extending the offline
A continuously cast steel billet is drawn past a cutter. **Crystallizer anomalies create
0.8 m scrap segments, but the agent is only told about a segment when it is within
-`reveal_lead` metres of the cut line** (default 8.0 m → hidden anomalies; `reveal_lead = 60`
-is the "faithful" setting where the agent sees all relevant defects). The agent is invoked
+`reveal_lead` metres of the cut line** (final config `reveal_lead = 10` → hidden anomalies;
+`reveal_lead = 60` is the "faithful" setting where the agent sees all relevant defects). The agent is invoked
*once per cut decision* with the **current visible state only** — it never sees future
defects — and must choose the next cut length. At the end the plan is scored against the
**full hidden defect set**: any piece overlapping a scrap segment is contaminated and fully
@@ -88,6 +88,12 @@ candidate rejected, runtime generation), and sandbox-evaluator consistency.
## Integrity / threat model
+- **The instance data (which embeds the full hidden defect schedule) is NOT copied into the
+ sandbox.** `frontier_eval/copy_files.txt` excludes `verification/data/instances`; the
+ evaluator loads the fixed instances from the host source benchmark dir
+ (`FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR`), so a candidate can never read the hidden
+ defects out of a data file (it only ever gets the visible `state`). This is the core
+ anti-cheat for the online task.
- `verification/ref_solver.py` and `verification/generator.py` are **not** copied into the
sandbox and are additionally forbidden by the validator (`ref_solver`, `generator`,
`anomaly_seed` tokens).
@@ -133,6 +139,11 @@ openevolve reaches the (offline) optimal exactly. The per-framework spread is sm
(std ≈ 0.4–1.2), so agent scores settle around the "safe short-cut" plateau (~70) that limits how
far an online agent can get without full foresight.
+> Methodological note: "online agents do not reach the clairvoyant optimum" is an **empirical**
+> observation (3 frameworks × 3 runs each are all below 76.4), not a proven structural lower bound.
+> To establish it rigorously one would add an "online oracle" reference layer — the best strategy
+> that uses only the revealed information (respecting `reveal_lead`) — and show agents fall below it.
+
> Honest note: agents improve in step-jumps (stuck at baseline for several generations, then a
> single mutation cracks ~70), not gradual climbing — consistent with a hard constraint where
> "get the strategy right once" beats incremental search. AB-MCTS's single high run (72.31) vs its
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
index 9edbdfd2..9cd37e81 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
@@ -3,7 +3,7 @@
一个**原创**的 Frontier-Engineering 基准,是离线版
[CuttingOptimization](../CuttingOptimization/README_zh-CN.md) 的**在线(闭环)**扩展,题材取自 2021 全国大学生数学建模竞赛 D 题。
-一根连续浇铸的钢坯被拉过切割机。**结晶器异常会产生 0.8m 报废段,但 agent 只在报废段距切割线 `reveal_lead` 米以内时才被告知**(默认 8m → 隐藏异常;`reveal_lead=60` 为"忠实版",此时 agent 能看到所有相关异常)。agent **每个切割决策点被调用一次、只拿到当下可见状态**,永远看不到未来,并要选下一刀长度。最后按**完整隐藏报废表**评分:任何与报废段重叠的切块都被污染、整块报废。
+一根连续浇铸的钢坯被拉过切割机。**结晶器异常会产生 0.8m 报废段,但 agent 只在报废段距切割线 `reveal_lead` 米以内时才被告知**(本基准最终配置 `reveal_lead=10` → 隐藏异常;`reveal_lead=60` 为"忠实版",此时 agent 能看到所有相关异常)。agent **每个切割决策点被调用一次、只拿到当下可见状态**,永远看不到未来,并要选下一刀长度。最后按**完整隐藏报废表**评分:任何与报废段重叠的切块都被污染、整块报废。
完整规则与评测语义见 [Task.md](./Task.md)。
@@ -106,8 +106,9 @@ python -m unittest discover -s verification -p "test_*.py"
> "强约束下把策略一次想对"胜过增量搜索。AB-MCTS 那次高分(72.31)与其 std(1.19)反映的是
> 运行间随机性,不是系统性优势。
-> 诚实说明:agent 改进呈**阶梯式跳变**(卡 baseline 多代、某次突变到 ~68-72),不是渐进爬升
-> ——符合"强约束下把策略一次想对"胜过增量搜索。
+> 方法学说明:以上"在线 agent 无法达到全知最优"是**经验观察**(3 框架 × 3 次均在 ref 之下),
+> 尚无"在线策略最优下界"的严格证明;若需坐实,可另加一个"只限可见信息、按揭示窗口保守决策"的
+> 在线 oracle 层作为参照。
## Docker
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md
index 74977f31..0f72047c 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/Task.md
@@ -10,10 +10,10 @@
- 拉坯速度 `v = 1.0` m/min;切一块 `tc = 3` min + 回程 `tr = 1` min;结晶器→切割机 `D = 60` m。
- 报废段长 `scrap_len = 0.8` m。
- 切长窗口:能运 `[min_basic, max_basic] = [4.8, 12.6]`;下道可接 `[min_process, max_process] = [8.0, 11.6]`;用户 `target` + 窗口 `[target_min, target_max]`。
-- **揭示距离 `reveal_dist = R`(默认 8.0 m)**:一段报废料仅在距离当前切割启动点 ≤ `R` 米时,才"揭示"给 agent。
+- **揭示距离 `reveal_lead = R`(最终配置默认 `10.0` m)**:一段报废料仅在距离当前切割启动点 ≤ `R` 米时,才"揭示"给 agent。(单位即"米";v=1.0 时它也等于"提前的分钟数",文档简称"米"。)
- 因 `v×(tc+tr)=4m < 4.8m`,切割机总能跟上 → **无吞吐瓶颈**。时间只在"揭示与到位"时差上起作用,不构成物理约束。
-> 为什么 `R < max_basic`:若把折现距离设成原题的 60m 缓冲(远大于 12.6m 切段),agent 在承诺每块之前总能看见该块内所有异常,从而永远主动避开 → 退化为离线。取 `R` 小于最大切段,使"远端未知带"存在,才产生真实不确定性。这是本任务与离线版难度分界的核心旋钮。
+> 为什么 `R < max_basic`:若把揭示距离设成原题的 60m 缓冲(远大于 12.6m 切段),agent 在承诺每块之前总能看见该块内所有异常,从而永远主动避开 → 退化为离线。取 `R` 小于最大切段,使"远端未知带"存在,才产生真实不确定性。这是本任务与离线版难度分界的核心旋钮。
## 2. 材料坐标与时序
@@ -21,24 +21,25 @@
异常由隐藏种子确定:一段报废料占据流区间 `[x_a, x_a+0.8]`。它在**距切割点 ≤ R 时揭示**(即当 `x_a ≤ cut_pos + R`),并将其位置/到达时间加入 agent 可见列表。
-## 3. 求解接口(闭环 REPL)
+## 3. 求解接口(每决策点一次调用)
-agent 程序**启动一次**:`python solver.py `(instance 里**不含任何异常/异常种子**),进入循环:
+agent 程序被评测器**每个切割决策点调用一次**:`python solver.py ` → stdout 打印决策。
-- 从 stdin 读一行**状态 JSON**:
+- 从 `state.json`(不含任何异常/异常种子,且**不含完整报废表**)读**当前决策点状态**,字段如下:
```json
-{"cut_pos": 12.6, "committed": [9.1, 11.4], "visible_defects": [{"x": 20.0}], "target": 9.5,
- "target_min": 9.0, "target_max": 10.0, "limits": {"min_basic":4.8,"max_basic":12.6,"min_process":8.0,"max_process":11.6},
- "total_length": 106.2}
+{"cut_pos": 12.6, "committed": [9.1, 11.4], "visible_defects": [{"x": 20.0, "x_end": 20.8}],
+ "target": 9.5, "target_min": 9.0, "target_max": 10.0,
+ "limits": {"min_basic":4.8,"max_basic":12.6,"min_process":8.0,"max_process":11.6},
+ "total_length": 106.2, "reveal_lead": 10.0}
```
- `cut_pos`:当前切割启动点(下一段的起点)。
- `committed`:已承诺的切段长度列表(之和 = cut_pos)。
- - `visible_defects`:**已揭示**(距 cut_pos ≤ R)且尚未被切过/尚未过去的报废段流位置(只含 `x_a`,长度固定 0.8m)。
- - `reveal_dist`、`total_length`、process 等。
-- 向 stdout 写一行**决策 JSON**:`{"piece_length": L}`,`L ∈ [4.8, 12.6]`。
-- 评审器按流推进:`cut_pos += L`,重复喂状态/拿下一刀;当 `S - cut_pos ≤ 12.6` 时强制收尾(最后一段 = `S - cut_pos`,agent 无需作答)。
+ - `visible_defects`:**已揭示**(距 cut_pos ≤ `${reveal_lead}`)且尚未被切过/尚未过去的报废段流位置(含 `x` 起点与 `x_end`,长度固定 0.8m)。
+ - `reveal_lead`:本次实例的揭示距离(米)。
+- 向 stdout 打印一行**决策 JSON**:`{"piece_length": L}`,`L ∈ [4.8, 12.6]`。
+- 评测器按流推进:`cut_pos += L`,重复"喂状态 → 拿下一刀";当 `S - cut_pos ≤ 12.6` 时强制收尾(最后一段 = `S - cut_pos`,agent 无需作答)。
-agent **永远看不到** `x_a > cut_pos + R` 的异常(未揭示);也看不到完整报废表。`visible_defects` 只含已揭示的。
+agent **永远看不到** `x_a > cut_pos + reveal_lead` 的异常(未揭示);也看不到完整报废表。`visible_defects` 只含已揭示的。
## 4. 污染判定(不可规避的代价,难度来源)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt
index 79f8b1a5..10228058 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/frontier_eval/copy_files.txt
@@ -2,5 +2,4 @@ baseline
verification/evaluate.py
verification/simulator.py
verification/validator.py
-verification/data/instances
frontier_eval
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
index f8c1ec26..4044788e 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
@@ -132,6 +132,21 @@ def _run_one(prog: Path, inst_path: Path, time_budget: float, py: str, tmp: Path
"n_cuts": len(cuts)}
+def _instances_dir() -> Path:
+ """在线版实例目录:只从宿主源 benchmark 目录加载(不复制进沙箱)。
+
+ 在线版的关键是"隐藏报废表"——实例文件含完整 defects/anomaly_seed,绝不能进候选可见
+ 目录(否则候选可读 JSON 拿到全知解作弊)。因此在沙箱内(本地无 data/instances 时)改从
+ FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR 加载;直跑(非沙箱)时回退到本地目录。
+ """
+ src = os.environ.get("FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR", "").strip()
+ if src:
+ host = Path(src) / "verification" / "data" / "instances"
+ if host.is_dir():
+ return host
+ return DATA_DIR
+
+
def evaluate(program_path: str, *, time_budget: float = 60.0, python: str | None = None,
data_dir: str | Path | None = None, reveal_lead: float | None = None) -> dict[str, Any]:
prog = Path(program_path).resolve()
@@ -140,10 +155,10 @@ def evaluate(program_path: str, *, time_budget: float = 60.0, python: str | None
violations = check_candidate(prog)
- inst_dir = Path(data_dir).resolve() if data_dir else DATA_DIR
+ inst_dir = Path(data_dir).resolve() if data_dir else _instances_dir()
instances = sorted(inst_dir.glob("instance_*.json")) if inst_dir.is_dir() else []
if not instances:
- # 若无固定实例集,现场生成一批(便于直接评测)
+ # 若无固定实例集,现场生成一批(便于直接评测;仅宿主,不进沙箱)
tmp = Path(tempfile.mkdtemp(prefix="oc_inst_"))
instances = _generate_instances(7, 6, tmp, reveal_lead or 10.0)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py
index 72e484fd..771dc7df 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/generator.py
@@ -24,7 +24,7 @@
LIMITS = {"min_basic": 4.8, "max_basic": 12.6, "min_process": 8.0, "max_process": 11.6}
-def generate(seed: int, difficulty: str = "medium", reveal_lead: float = 60.0,
+def generate(seed: int, difficulty: str = "medium", reveal_lead: float = 10.0,
n_anomaly: int | None = None) -> dict[str, Any]:
rng = random.Random(seed * 10007 + 11)
if difficulty == "easy":
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
index b204a48a..6ef23790 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
@@ -148,7 +148,9 @@ def score(inst: dict[str, Any], cuts: list[float]) -> tuple[bool, dict[str, Any]
total_scrap += max(0.0, c - float(inst["customer"]["target_max"]))
total_penalty += abs(min(c, float(inst["customer"]["target_max"]))
- float(inst["customer"]["target"]))
- util = max(0.0, 100.0 * (S - total_scrap) / S)
+ # util 含极小贴合度惩罚(破平),与离线版口径一致:scrap 先、penalty 破平
+ lam = float(inst.get("limits", {}).get("target_penalty_weight", 1e-4))
+ util = max(0.0, 100.0 * (S - (total_scrap + lam * total_penalty)) / S)
return True, {"valid": True, "scrap": round(total_scrap, 4),
"penalty": round(total_penalty, 4), "util": round(util, 2),
"cuts": cuts}
From 3394acc0fa10cf41308369f38642e34f4bf83cbc Mon Sep 17 00:00:00 2001
From: zzy <17092805+mz2007@user.noreply.gitee.com>
Date: Sat, 5 Sep 2026 17:37:28 +0800
Subject: [PATCH 3/6] fix: align online tooling/defaults with final
reveal_lead=10 config
Address second reviewer pass on CuttingOptimizationOnline:
- multiseed_stat.py: DEFAULT_REFERENCE 88.72 (offline) -> 76.37 (online clairvoyant), and the
docstring/usage example now point at the online run dir; README notes the tool aggregates
every run in the dir (restrict via sub-glob for config-specific stats).
- simulator.py: DEFAULTS reveal_lead 60.0 -> 10.0 (final config; no silent 'faithful' default),
docstring updated for the penalty-inclusive util formula.
- Tie-break is no longer rounded away: simulator keeps util at 4 decimals and evaluate rounds
combined_score to 3 decimals, so the 1e-4 target-fit penalty (order ~1e-3 util) actually
discriminates equal-scrap plans (matches the offline formula).
---
.../CuttingOptimizationOnline/README.md | 2 ++
.../verification/evaluate.py | 3 ++-
.../verification/multiseed_stat.py | 6 +++---
.../verification/simulator.py | 17 ++++++++++-------
4 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
index 060e2d0a..2a0f32b9 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
@@ -58,6 +58,8 @@ $env:ONLINE_CUT_EVAL_GENERATE_SEED = ""
python verification/evaluate.py baseline/solver.py
# Multi-run stats (mean ± std) across a framework run dir
+# Multi-run stats — note the tool aggregates every run in the dir; point it at the full run dir
+# (it defaults to the online clairvoyant reference 76.37), or pass a sub-glob to restrict.
python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimizationOnline/openevolve
```
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
index 4044788e..f5d4aa5b 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/evaluate.py
@@ -215,7 +215,8 @@ def evaluate(program_path: str, *, time_budget: float = 60.0, python: str | None
shutil.rmtree(tmp, ignore_errors=True)
combined = total / len(instances) if instances else 0.0
- return {"combined_score": round(combined, 2), "valid": 1.0 if all_valid and not violations else 0.0,
+ # 保留 3 位:贴合度惩罚量级 ~1e-3,2 位舍入会把破平项抹掉,故至少 3 位
+ return {"combined_score": round(combined, 3), "valid": 1.0 if all_valid and not violations else 0.0,
"per_instance": per_instance, "num_instances": len(instances),
"time_budget_s": time_budget, "generate_seed": gen_seed_raw or None}
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
index e6df657a..0e8cb18b 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
@@ -1,9 +1,9 @@
"""多运行(多种子/多轮)agent 分数统计:对一组框架运行目录取 combined_score 的 mean±std。
用法:
- python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimization/openevolve
+ python verification/multiseed_stat.py --runs-dir runs/unified__ContinuousCasting__CuttingOptimizationOnline/openevolve
python verification/multiseed_stat.py --runs-dir "runs/**/openevolve/deepseek-v4-flash" # glob
- python verification/multiseed_stat.py --runs-dir --pattern "*openevolve*"
+ python verification/multiseed_stat.py --runs-dir --pattern "*openevolve*" --reference 76.37
从每个运行目录下读 `/best/best_program_info.json` 的 `metrics.combined_score`
(框架统一保存的 best 程序分数),对多次运行做 mean / std / min / max,
@@ -22,7 +22,7 @@
import statistics
from pathlib import Path
-DEFAULT_REFERENCE = 88.72 # 全知参考解利用率(verification/ref_solver.py),可 --reference 覆盖
+DEFAULT_REFERENCE = 76.37 # 在线版全知参考解利用率(verification/ref_solver.py),可 --reference 覆盖
def _best_score(run_dir: Path) -> float | None:
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
index 6ef23790..96c660eb 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/simulator.py
@@ -3,14 +3,16 @@
模型(详见 Task.md):
- 材料用流坐标 x∈[0,S],S = v*浇铸时长。横截面 x 到达切割点在 x/v + D/v。
- 隐藏异常(由 anomaly_seed 决定):报废段占流区间 [x_a, x_a+scrap_len]。
-- 揭示提前量 reveal_lead(分钟;v=1 时等于"上游米数"):一段报废料在
- x_a <= cut_pos + reveal_lead 时"揭示"给 agent;否则不可见。reveal_lead=60 为忠实版
- (τ_a 即知,60m 提前量);reveal_lead= min_basic 的
污染块里、整块报废(不允许像离线版那样把报废段当 0.8m 小块切出)。
- 评分:scrap = 所有污染块长度 + 干净块超窗口报废 + 干净块 tuple[bool, dict[str, Any]
total_scrap += max(0.0, c - float(inst["customer"]["target_max"]))
total_penalty += abs(min(c, float(inst["customer"]["target_max"]))
- float(inst["customer"]["target"]))
- # util 含极小贴合度惩罚(破平),与离线版口径一致:scrap 先、penalty 破平
+ # util 含极小贴合度惩罚(破平),与离线版口径一致:scrap 先、penalty 破平。
+ # 保留到 4 位(不低于 penalty 量级),否则惩罚项会被 2 位舍入吞掉、破平失效。
lam = float(inst.get("limits", {}).get("target_penalty_weight", 1e-4))
util = max(0.0, 100.0 * (S - (total_scrap + lam * total_penalty)) / S)
return True, {"valid": True, "scrap": round(total_scrap, 4),
- "penalty": round(total_penalty, 4), "util": round(util, 2),
+ "penalty": round(total_penalty, 4), "util": round(util, 4),
"cuts": cuts}
From 24bc779ec9c26cb5bbf1066813df456827757ca3 Mon Sep 17 00:00:00 2001
From: zzy <17092805+mz2007@user.noreply.gitee.com>
Date: Sat, 5 Sep 2026 17:48:26 +0800
Subject: [PATCH 4/6] docs: fix --reference help default text (88.72 -> 76.37)
in online multiseed_stat
---
.../CuttingOptimizationOnline/verification/multiseed_stat.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
index 0e8cb18b..416192c1 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/multiseed_stat.py
@@ -64,7 +64,7 @@ def main() -> int:
parser.add_argument("--runs-dir", required=True, help="运行目录或 glob 模式")
parser.add_argument("--pattern", default=None, help="追加的子 glob 模式")
parser.add_argument("--reference", type=float, default=DEFAULT_REFERENCE,
- help="参考解利用率(默认 88.72)")
+ help="参考解利用率(默认 76.37)")
args = parser.parse_args()
pairs = collect(args.runs_dir, args.pattern)
From 1881fd30416892ba4882679c22b8ce83a5af6bc8 Mon Sep 17 00:00:00 2001
From: zzy <17092805+mz2007@user.noreply.gitee.com>
Date: Sat, 5 Sep 2026 18:06:02 +0800
Subject: [PATCH 5/6] docs: soften 'cannot reach clairvoyant' to empirical
wording in TASK_DETAILS
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Align TASK_DETAILS rows with the Task/README framing: the online info-asymmetry claim is an
observed effect (3 frameworks x 3 runs each below the clairvoyant ceiling), not a proven lower
bound. EN/ZH rows now use 'empirically keeps below' / '(经验上)低于' wording.
---
TASK_DETAILS.md | 2 +-
TASK_DETAILS_zh-CN.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/TASK_DETAILS.md b/TASK_DETAILS.md
index c2d2c98b..86ae747d 100644
--- a/TASK_DETAILS.md
+++ b/TASK_DETAILS.md
@@ -353,7 +353,7 @@ We welcome new engineering problem ideas — even without complete verification
CuttingOptimizationOnline |
- Online (closed-loop) cutting where 0.8 m scrap segments are revealed only within a reveal horizon — info asymmetry keeps agents below the clairvoyant optimum |
+ Online (closed-loop) cutting where 0.8 m scrap segments are revealed only within a reveal horizon — info asymmetry empirically keeps agents below the clairvoyant optimum (observed, not a proven lower bound) |
| AdditiveManufacturing |
diff --git a/TASK_DETAILS_zh-CN.md b/TASK_DETAILS_zh-CN.md
index 5e9f0559..e1a0dee7 100644
--- a/TASK_DETAILS_zh-CN.md
+++ b/TASK_DETAILS_zh-CN.md
@@ -353,7 +353,7 @@ Frontier-Eng 目前已覆盖以下领域的任务。每个任务均配有可运
CuttingOptimizationOnline |
- 在线(闭环)切割:0.8m 报废段只在揭示提前量内才告知 agent——信息不对称让 agent 达不到全知最优 |
+ 在线(闭环)切割:0.8m 报废段只在揭示提前量内才告知 agent——信息不对称使 agent(经验上)低于全知最优(观察结果,非严格下界证明) |
| AdditiveManufacturing |
From ab1bfc2d14a0ab0edfaadba7372099130e69c592 Mon Sep 17 00:00:00 2001
From: zzy <17092805+mz2007@user.noreply.gitee.com>
Date: Sat, 5 Sep 2026 18:17:30 +0800
Subject: [PATCH 6/6] docs: record WSL docker-isolation verification (both
tasks)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ran unified runtime in task.runtime.isolation_mode=docker under WSL for both benchmarks
(FRONTIER_EVAL_UNIFIED_DOCKER_USER=1000:1000, no shell override, docker_image built from the
task Dockerfile). Offline baseline scored 72.49 / valid=1.0 and online baseline 52.40 / valid=1.0,
identical to process mode — so docker isolation scoring is verified (not just documented), and for
the online task this also confirms the anti-cheat holds in-container (fixed instances with the
hidden defect schedule stay out of the sandbox; the evaluator reads them from the container's
mounted source repo).
---
.../CuttingOptimization/README.md | 17 +++++++++++--
.../CuttingOptimization/README_zh-CN.md | 6 +++++
.../CuttingOptimizationOnline/README.md | 24 +++++++++++++++----
.../CuttingOptimizationOnline/README_zh-CN.md | 8 ++++---
4 files changed, 45 insertions(+), 10 deletions(-)
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/README.md b/benchmarks/ContinuousCasting/CuttingOptimization/README.md
index a540988f..ccf5ae3f 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimization/README.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/README.md
@@ -61,10 +61,23 @@ The evaluator is pure stdlib, so a minimal `python` image suffices. Build it and
unified runtime's `isolation_mode=docker`:
```bash
-# Build (inside the CuttingOptimization directory)
-docker build -t cutting-opt-benchmark -f verification/docker/Dockerfile .
+# Build (from the repo root)
+docker build -t cutting-opt-benchmark -f benchmarks/ContinuousCasting/CuttingOptimization/verification/docker/Dockerfile benchmarks/ContinuousCasting/CuttingOptimization
+
+# Score the baseline under docker isolation (WSL/Linux; Windows hosts are limited by a
+# framework path bug). docker mode must NOT set task.runtime.shell; set DOCKER_USER so the
+# container can write the WSL /tmp sandbox.
+FRONTIER_EVAL_UNIFIED_DOCKER_USER=1000:1000 \
+ .venvs/frontier-eval-driver-wsl/bin/python -m frontier_eval task=unified \
+ task.benchmark=ContinuousCasting/CuttingOptimization algorithm=openevolve algorithm.iterations=0 \
+ llm.timeout=600 task.runtime.isolation_mode=docker task.runtime.docker_image=cutting-opt-benchmark
```
+**Verified (WSL, docker isolation)**: baseline `combined_score=72.49, valid=1.0, num_instances=8`
+— identical to process mode. `docker` scoring works under WSL/Linux because `eval_command.txt`
+injects `FRONTIER_EVAL_UNIFIED_SOURCE_BENCHMARK_DIR={benchmark_source}`, which the container
+resolves to the mounted repo path (no framework env-forwarding needed).
+
## Tests
```powershell
diff --git a/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md b/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
index 86f4b89b..c4d27d9c 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimization/README_zh-CN.md
@@ -47,6 +47,12 @@ python verification/evaluate.py baseline/solver.py --generate-seed
python verification/evaluate.py baseline/solver.py --time-budget 10
```
+## Docker
+
+提供最简 `python:3.11-slim` 镜像(`verification/docker/Dockerfile`)。构建后用 unified 的
+`isolation_mode=docker`(WSL/Linux;Windows 宿主受框架路径 bug 限制)。**已在 WSL 下实测通过**:
+基线 `combined_score=72.49, valid=1.0, num_instances=8`,与 process 模式一致。
+
## 测试
```powershell
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
index 2a0f32b9..8fe4b852 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README.md
@@ -157,8 +157,22 @@ far an online agent can get without full foresight.
## Docker
-A minimal `python:3.11-slim` image is provided (`verification/docker/Dockerfile`). Docker
-isolation scoring depends on the shared Frontier-Eng framework's env-forwarding, which is a
-known framework-level limitation (the reference path env may not reach the container).
-`docker` isolation is therefore best verified under WSL/Linux with the unified runtime's
-`isolation_mode=docker`; on Windows hosts it is limited by a framework path bug.
+A minimal `python:3.11-slim` image is provided (`verification/docker/Dockerfile`). Build it
+and run the unified runtime in `isolation_mode=docker` (WSL/Linux; Windows hosts are limited
+by a framework path bug). docker mode must NOT set `task.runtime.shell`; set
+`FRONTIER_EVAL_UNIFIED_DOCKER_USER=1000:1000` so the container can write the WSL `/tmp`
+sandbox:
+
+```bash
+docker build -t cutting-opt-online -f benchmarks/ContinuousCasting/CuttingOptimizationOnline/verification/docker/Dockerfile benchmarks/ContinuousCasting/CuttingOptimizationOnline
+FRONTIER_EVAL_UNIFIED_DOCKER_USER=1000:1000 \
+ .venvs/frontier-eval-driver-wsl/bin/python -m frontier_eval task=unified \
+ task.benchmark=ContinuousCasting/CuttingOptimizationOnline algorithm=openevolve algorithm.iterations=0 \
+ llm.timeout=600 task.runtime.isolation_mode=docker task.runtime.docker_image=cutting-opt-online
+```
+
+**Verified (WSL, docker isolation)**: baseline `combined_score=52.40, valid=1.0, num_instances=8`
+— identical to process mode. This also confirms the online anti-cheat holds in docker: the fixed
+instances (with the hidden defect schedule) are **not** copied into the sandbox; the evaluator
+loads them from the host source benchmark dir (`{benchmark_source}` → the mounted repo path in the
+container), so the candidate in the container never reads the hidden defects.
diff --git a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
index 9cd37e81..4a9b2b8f 100644
--- a/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
+++ b/benchmarks/ContinuousCasting/CuttingOptimizationOnline/README_zh-CN.md
@@ -112,6 +112,8 @@ python -m unittest discover -s verification -p "test_*.py"
## Docker
-提供最简 `python:3.11-slim` 镜像(`verification/docker/Dockerfile`)。Docker 隔离评分依赖共享
-Frontier-Eng 框架的 env 转发(已知框架级限制,参考路径 env 可能进不了容器)。`docker` 隔离最好在
-WSL/Linux 下用 unified 的 `isolation_mode=docker` 验证;Windows 宿主受框架路径 bug 限制。
+提供最简 `python:3.11-slim` 镜像(`verification/docker/Dockerfile`)。构建镜像后用 unified 的
+`isolation_mode=docker`(WSL/Linux;Windows 宿主受框架路径 bug 限制)。**已在 WSL 下实测通过**:
+基线 `combined_score=52.40, valid=1.0, num_instances=8`,与 process 模式一致。这也再次验证了在线版
+防作弊在容器里同样成立——固定实例(含隐藏报废表)**不进沙箱**,评测器从宿主源 benchmark 目录
+(`{benchmark_source}` → 容器内挂载的 repo 路径)加载,候选在容器内读不到隐藏报废表。