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

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

557 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { Type, type Static } from "typebox";
import { Value } from "typebox/value";
import type {
HypothesisMetric,
Json,
JudgedResult,
Measured,
MetricDefinition,
ProcessResult,
RunDefinition,
} from "../vocab/index.ts";
import type { TaskFacts } from "./facts.ts";
import { measured, readMetricsFile, unmeasured } from "./metrics-file.ts";
import type {
DiagnosisEnding,
DiagnosisExperiment,
DiagnosisExperimentJudged,
DiagnosisJudged,
DiagnosisJudgingStep,
LightCheckOutcome,
Task,
TaskImplementation,
} from "./types.ts";
// 填圆:在单位正方形里放 26 个圆,最大化半径之和。任务说明见 tasks/circle_packing/TASK.md。
// 凡是要执行任务代码的事(正式运行、校验器、轻量检查)都只交出运行定义,由调用方交给 runner 在任务镜像里跑;
// 这里只读写文件、解读产物(docs/结构.md 第二部分"runner 只管怎么跑,tasks 只管怎么判")。
const CIRCLES = 26;
const ENTRY = "initial.py";
const HARNESS = "harness.py";
const DIAGNOSIS_HARNESS = "harness-diagnosis.py";
const VALIDATOR = "validate.py";
const CHECK_SCRIPT = "check-candidate.py";
const METRICS_SPEC = "metrics-spec.json";
const METRICS = "metrics.json";
const SOLUTION = "solution.json";
const VERDICT = "verdict.json";
const CHECK_RESULT = "check.json";
/** 诊断运行:原始产物目录、每个实验的答案目录、harness 写的每实验用时报告(路径都相对运行目录)。 */
const BASELINE_ARTIFACT = "baseline-artifact";
const ANSWERS = "answers";
const DIAGNOSIS_REPORT = "diagnosis.json";
/** 判分目录里每个实验写的校验器退出码文件(同一个目录下还有这个实验的 solution.json、verdict.json)。 */
const EXIT_FILE = "exit";
/** 轻量检查目录里放候选代码的子目录;检查脚本放在它的上一层,免得扫到自己。 */
const CHECK_CANDIDATE = "candidate";
/** 运行目录挂进容器的位置。 */
const WORKDIR = "/work";
/** 校验器的时限。26 个圆两两比较加合法化,实测一秒以内;它超时说明宿主的工具出了问题。 */
const VALIDATOR_SECONDS = 60;
/** 合法化前先按容差 0 判原产物(方法.md 第八部分"合法化"第 1 条)。 */
const TOLERANCE = "0";
// 常规辅助指标的编号(方法.md 第五部分"指标")。前三个来自 runner 的资源用量,后四个来自校验器。
const WALL = "wall-seconds";
const CPU = "cpu-seconds";
const MEMORY = "peak-memory-bytes";
const CLEARANCE = "min-pairwise-clearance";
const RAW_SCORE = "raw-score";
const RAW_VIOLATION = "raw-violation";
const DEDUCTION = "legalization-deduction";
const FROM_VALIDATOR = [CLEARANCE, RAW_SCORE, RAW_VIOLATION, DEDUCTION];
const SIDE = "单位正方形的边长";
const PER_RUN = "每次运行结束后由宿主读一次,直接取值";
function auxiliary(
id: string,
meaning: string,
unit: string,
direction: "maximize" | "minimize",
howMeasured: string,
): MetricDefinition {
return { id, layer: "auxiliary", meaning, unit, direction, howMeasured, missing: "recordMissing" };
}
function metricDefinitions(facts: TaskFacts): MetricDefinition[] {
const validator = `宿主的校验器从 ${SOLUTION} 重算,${PER_RUN}`;
return [
{
id: facts.finalMetric.id,
layer: "final",
meaning:
"合法摆法的半径之和,由宿主的独立校验器从实际产物重算;原产物差一点不合法时,先按 TASK.md 的只缩不移规则修,再按修后的半径算",
unit: SIDE,
direction: facts.finalMetric.direction,
howMeasured: validator,
missing: "invalidateRun",
},
auxiliary(WALL, "正式运行的墙钟时间", "秒", "minimize", `容器里的计量程序记录,${PER_RUN}`),
auxiliary(CPU, "正式运行用掉的 CPU 秒(容器 cgroup 的 usage_usec)", "秒", "minimize", `容器里的计量程序记录,${PER_RUN}`),
auxiliary(MEMORY, "正式运行的峰值内存(容器 cgroup 的 memory.peak)", "字节", "minimize", `容器里的计量程序记录,${PER_RUN}`),
auxiliary(CLEARANCE, "所有圆对中「圆心距减两半径之和」的最小值,大于等于 0 表示不重叠;修过的取修后的摆法", SIDE, "maximize", validator),
auxiliary(RAW_SCORE, "合法化之前原产物的半径之和", SIDE, "maximize", validator),
auxiliary(RAW_VIOLATION, "原产物的最大违反量:两圆重叠的量和圆出边的量里最大的一个,0 表示没有违反", SIDE, "minimize", validator),
auxiliary(DEDUCTION, "只缩不移合法化扣掉的分:原分数减最终成绩,原产物合法时是 0", SIDE, "minimize", validator),
];
}
/** 正式运行、校验器、轻量检查共用的运行定义。数学库的线程数和核数一致,否则会开多个线程抢核。 */
function definition(task: Task, imageDigest: string, command: string[], wallSeconds: number, env: Record<string, string>): RunDefinition {
const cores = String(task.facts.run.cores);
return {
command,
workdir: WORKDIR,
image: { digest: imageDigest, runtime: task.facts.run.runtime },
wallSeconds,
cores: task.facts.run.cores,
inheritEnv: false,
env: { OMP_NUM_THREADS: cores, OPENBLAS_NUM_THREADS: cores, MKL_NUM_THREADS: cores, ...env },
};
}
// 解释器不加 -I:-I 会忽略全部 PYTHON* 环境变量,连同 runner 设的 PYTHON_CPU_COUNT,
// 于是 os.cpu_count() 报回宿主核数(本机实测 16)。容器里的环境变量全部由运行定义给出,用不着 -I。
// -B:不写 __pycache__,不往运行目录里添文件。
function prepareRun(task: Task, candidateDir: string, runDir: string, hypothesisMetrics: HypothesisMetric[]): void {
cpSync(candidateDir, runDir, { recursive: true });
// harness 和指标说明以宿主的为准,候选目录里有同名文件也被盖掉(这两个名字在 task.json 的不许改名单里)。
copyFileSync(join(task.dir, HARNESS), join(runDir, HARNESS));
writeFileSync(join(runDir, METRICS_SPEC), `${JSON.stringify(hypothesisMetrics, null, 2)}\n`);
}
function formalRun(task: Task, imageDigest: string, seed: string): RunDefinition {
const command = ["python", "-B", `${WORKDIR}/${HARNESS}`, "--program", ENTRY, "--output", SOLUTION];
return definition(task, imageDigest, command, task.facts.run.wallSeconds, { INFORSI_SEED: seed });
}
function lightCheck(task: Task, candidateDir: string, checkDir: string, imageDigest: string, seconds: number): RunDefinition {
cpSync(candidateDir, join(checkDir, CHECK_CANDIDATE), { recursive: true });
copyFileSync(join(task.dir, CHECK_SCRIPT), join(checkDir, CHECK_SCRIPT));
const command = ["python", "-B", CHECK_SCRIPT, CHECK_CANDIDATE, ENTRY, CHECK_RESULT];
return definition(task, imageDigest, command, seconds, {});
}
/** 检查脚本写的结果文件。valid 与 reasons 是否为空必须一致。 */
const CheckResult = Type.Object({ valid: Type.Boolean(), reasons: Type.Array(Type.String()) });
/** 宿主的工具(检查脚本、校验器)没正常跑完时报错停下:这不是候选的失败(任务书 05 第三部分)。 */
function requireExit(what: string, result: ProcessResult, codes: number[]): number {
if (result.ending === "exited" && codes.includes(result.exitCode)) return result.exitCode;
const how = result.ending === "notStarted" ? `没启动:${result.reason}` : result.ending === "exited" ? `退出码 ${result.exitCode}` : result.ending;
throw new Error(`${what}没有正常跑完(${how}),这是宿主一侧的工具出了问题,不能当成候选的失败`);
}
function readJson(path: string): unknown {
try {
return JSON.parse(readFileSync(path, "utf8"));
} catch (error) {
throw new Error(`读不出 ${path}:${(error as Error).message}`, { cause: error });
}
}
function readLightCheck(checkDir: string, result: ProcessResult): LightCheckOutcome {
requireExit("轻量检查脚本", result, [0]);
const path = join(checkDir, CHECK_RESULT);
const check = readJson(path);
if (!Value.Check(CheckResult, check) || check.valid !== (check.reasons.length === 0)) {
throw new Error(`${path} 的内容认不出:${JSON.stringify(check)}`);
}
return check.valid ? { passed: true } : { passed: false, reasons: check.reasons };
}
/** 正式运行没正常结束的原因;正常退出(退出码 0)时为 null。 */
function processFailure(task: Task, formal: Exclude<ProcessResult, { ending: "notStarted" }>): string | null {
if (formal.ending === "timeout") return `超过单次时限 ${task.facts.run.wallSeconds} 秒,被结束`;
if (formal.ending === "cancelled") return "容器从外面被结束(关机、手动 docker kill 等),没有留下计量结果";
if (formal.exitCode !== 0) return `正式运行以退出码 ${formal.exitCode} 结束`;
return null;
}
function started(formal: ProcessResult): Exclude<ProcessResult, { ending: "notStarted" }> {
if (formal.ending === "notStarted") throw new Error(`没启动的运行不判分:${formal.reason}`);
return formal;
}
/**
* 拼出 JudgedResult:资源用量来自正式运行,假设专用指标和两个保留键来自候选写的 metrics.json。
* 有"缺了整次作废"的假设专用指标测不到时,原本算数的也改判不算数(方法.md 第五部分"指标")。
*/
function assemble(
runDir: string,
formal: Exclude<ProcessResult, { ending: "notStarted" }>,
hypothesisMetrics: HypothesisMetric[],
verdict: { valid: boolean; reason: string },
score: Measured,
fromValidator: Record<string, Measured>,
extra: Json,
): JudgedResult {
const file = readMetricsFile(join(runDir, METRICS), METRICS, hypothesisMetrics);
const voided = hypothesisMetrics.flatMap((m) => {
const value = file.hypothesis[m.id]!;
return m.missing === "invalidateRun" && value.status === "unmeasured" ? [`${m.id}(${value.reason})`] : [];
});
const result = verdict.valid && voided.length > 0
? { valid: false, reason: `登记为"缺了整次作废"的假设专用指标测不到:${voided.join(";")}` }
: verdict;
return {
verdict: result.valid ? "valid" : "invalid",
verdictReason: result.reason,
score,
metrics: {
[WALL]: formal.wallSeconds,
[CPU]: formal.cpuSeconds,
[MEMORY]: formal.peakMemoryBytes,
...fromValidator,
...file.hypothesis,
},
answerPath: file.answerPath,
safeguardTriggers: file.safeguardTriggers,
extra,
};
}
function prepareJudging(
task: Task,
runDir: string,
judgeDir: string,
imageDigest: string,
formal: ProcessResult,
hypothesisMetrics: HypothesisMetric[],
): ReturnType<TaskImplementation["prepareJudging"]> {
const run = started(formal);
const solution = join(runDir, SOLUTION);
const failure = processFailure(task, run) ?? (existsSync(solution) ? null : `没有产物:运行目录里没有 ${SOLUTION}`);
if (failure !== null) {
const notValidated = Object.fromEntries(FROM_VALIDATOR.map((id) => [id, unmeasured(`没有跑校验器:${failure}`)]));
const result = assemble(runDir, run, hypothesisMetrics, { valid: false, reason: failure }, unmeasured(failure), notValidated, {
legalization: null,
});
return { status: "judged", result };
}
copyFileSync(solution, join(judgeDir, SOLUTION));
copyFileSync(join(task.dir, VALIDATOR), join(judgeDir, VALIDATOR));
const command = [
"python",
"-B",
VALIDATOR,
"--circles",
String(CIRCLES),
"--solution",
SOLUTION,
"--atol",
TOLERANCE,
"--legalize",
"--verdict",
VERDICT,
];
return { status: "validate", definition: definition(task, imageDigest, command, VALIDATOR_SECONDS, {}) };
}
const NumberOrNull = Type.Union([Type.Number(), Type.Null()]);
/** validate.py --legalize 写的判定里,这里要读的部分;其余键(evidence、legal_radii 等)不读。 */
const ValidatorVerdict = Type.Object({
valid: Type.Boolean(),
computed_sum: NumberOrNull,
reasons: Type.Array(Type.String()),
diagnostics: Type.Object({ min_pairwise_clearance: NumberOrNull }),
legalization: Type.Object({
raw_valid: Type.Boolean(),
raw_score: NumberOrNull,
raw_violation: NumberOrNull,
applied: Type.Boolean(),
repaired: Type.Boolean(),
shrunk_circles: Type.Array(Type.Integer()),
reasons: Type.Array(Type.String()),
}),
});
type ValidatorVerdict = Static<typeof ValidatorVerdict>;
/** 校验器在形状不对、含非有限数或负半径时提前返回,几何量没算。 */
const NOT_COMPUTED = "原产物形状不对、含非有限数或负半径,校验器没算这个量";
function orUnmeasured(value: number | null, reason: string): Measured {
return value === null ? unmeasured(reason) : measured(value);
}
function readVerdict(judgeDir: string, exitCode: number): ValidatorVerdict {
const path = join(judgeDir, VERDICT);
const verdict = readJson(path);
if (!Value.Check(ValidatorVerdict, verdict)) throw new Error(`${path} 的内容认不出:${JSON.stringify(verdict)}`);
// 退出码 0 = 合法、1 = 不合法;合法化之后的结论就是最终判定。对不上说明跑的不是这份校验器。
const consistent =
verdict.valid === (exitCode === 0) &&
verdict.legalization.repaired === verdict.valid &&
(!verdict.valid || verdict.computed_sum !== null);
if (!consistent) throw new Error(`${path} 与退出码 ${exitCode} 或自身前后对不上:${JSON.stringify(verdict)}`);
return verdict;
}
/** 校验器判定读成的结论:算不算数、理由、最终分数、校验器给的辅助指标、合法化明细。 */
interface Scored {
valid: boolean;
reason: string;
score: Measured;
fromValidator: Record<string, Measured>;
extra: Json;
}
function scoreVerdict(verdict: ValidatorVerdict): Scored {
const legal = verdict.legalization;
let deduction: Measured;
let score: Measured;
if (verdict.valid) {
const final = verdict.computed_sum!;
score = measured(final);
// legal.repaired 为真时 raw_score 一定有值:原产物能算出分数,才可能合法或被修好。
const raw = legal.raw_score!;
// 只缩半径只会降分;修后反而更高,说明修法写错了(方法.md 第八部分"合法化"第 3 条)。
if (final > raw) throw new Error(`合法化后分数 ${final} 高于原分数 ${raw}:只缩半径只会降分,修法出错了`);
deduction = measured(raw - final);
} else {
const reason = `不算数:${verdict.reasons.concat(legal.reasons).join(";")}`;
score = unmeasured(reason);
deduction = unmeasured(`修不了,没有修后分数:${legal.reasons.join(";")}`);
}
const rawScore = orUnmeasured(legal.raw_score, NOT_COMPUTED);
const rawViolation = orUnmeasured(legal.raw_violation, NOT_COMPUTED);
const fromValidator = {
[CLEARANCE]: orUnmeasured(verdict.diagnostics.min_pairwise_clearance, NOT_COMPUTED),
[RAW_SCORE]: rawScore,
[RAW_VIOLATION]: rawViolation,
[DEDUCTION]: deduction,
};
const reason = verdict.valid
? legal.raw_valid
? "校验器按容差 0 判原产物合法"
: `原产物不合法,按只缩不移规则修好后合法(缩了 ${legal.shrunk_circles.length} 个圆)`
: `校验器判不合法:${verdict.reasons.concat(legal.reasons).join(";")}`;
const extra = {
legalization: {
rawValid: legal.raw_valid,
applied: legal.applied,
repaired: legal.repaired,
shrunkCircles: legal.shrunk_circles,
reasons: legal.reasons,
rawScore,
rawViolation,
deduction,
},
};
return { valid: verdict.valid, reason, score, fromValidator, extra };
}
function judge(
_task: Task,
runDir: string,
judgeDir: string,
formal: ProcessResult,
validator: ProcessResult,
hypothesisMetrics: HypothesisMetric[],
): JudgedResult {
const run = started(formal);
const scored = scoreVerdict(readVerdict(judgeDir, requireExit("校验器", validator, [0, 1])));
return assemble(runDir, run, hypothesisMetrics, { valid: scored.valid, reason: scored.reason }, scored.score, scored.fromValidator, scored.extra);
}
// ---------------------------------------------------------------------------
// 诊断运行(方法.md 4.3"诊断"一段):一个容器、一次登记、一次扣次,里面按顺序跑几个实验。
// 全部是新增的一套函数,用自己的 harness(harness-diagnosis.py),不碰上面正式运行的任何一步。
// ---------------------------------------------------------------------------
/** 诊断程序存版本时跳过的名字:正式运行那份加上原始产物目录(它是宿主放进工作目录的,不是实验员的代码)。 */
function diagnosisUnversionedNames(facts: TaskFacts): string[] {
return [...facts.unversionedNames, BASELINE_ARTIFACT];
}
/**
* 把产出当前基线的那次正式运行的原始产物(solution.json)放进 dir 下的 baseline-artifact/,设成只读。
* 实验员的工作目录和诊断运行的运行目录都用它:实验员能看到实验要接着算的那份答案,运行时实验读的是同一份。
* 只读靠文件权限,防的是实验顺手改了它,不是安全边界。
*/
function placeBaselineArtifact(_task: Task, baselineRunDir: string, dir: string): void {
const target = join(dir, BASELINE_ARTIFACT);
mkdirSync(target);
const file = join(target, SOLUTION);
copyFileSync(join(baselineRunDir, SOLUTION), file);
chmodSync(file, 0o444);
}
function prepareDiagnosisRun(task: Task, programDir: string, baselineRunDir: string, runDir: string, experiments: DiagnosisExperiment[]): void {
cpSync(programDir, runDir, { recursive: true });
// 两份 harness 以宿主的为准;指标说明是全部实验登记的指标合在一起,每个实验只写自己的那几条。
copyFileSync(join(task.dir, HARNESS), join(runDir, HARNESS));
copyFileSync(join(task.dir, DIAGNOSIS_HARNESS), join(runDir, DIAGNOSIS_HARNESS));
writeFileSync(join(runDir, METRICS_SPEC), `${JSON.stringify(experiments.flatMap((e) => e.metrics), null, 2)}\n`);
placeBaselineArtifact(task, baselineRunDir, runDir);
mkdirSync(join(runDir, ANSWERS));
}
/** 总时限、核数照任务规定,实验按顺序跑,每个实验分到总时限 ÷ 实验数(由 harness 执行)。 */
function diagnosisRun(task: Task, imageDigest: string, seed: string, experimentIds: string[]): RunDefinition {
const eachSeconds = task.facts.run.wallSeconds / experimentIds.length;
const command = [
"python",
"-B",
`${WORKDIR}/${DIAGNOSIS_HARNESS}`,
"--program",
ENTRY,
"--baseline",
BASELINE_ARTIFACT,
"--answers",
ANSWERS,
"--seconds-each",
String(eachSeconds),
"--report",
DIAGNOSIS_REPORT,
"--experiments",
...experimentIds,
];
return definition(task, imageDigest, command, task.facts.run.wallSeconds, { INFORSI_SEED: seed });
}
/** 一个实验在运行目录里的结局:harness 报告里的一项,或者整次运行先结束、没轮到它。 */
type ExperimentOutcome = { ending: "finished" | "crashed" | "timeout"; wallSeconds: number } | { ending: "notRun"; reason: string };
/** harness 写的报告(我们自己的程序,按约定的格式直接读)。 */
interface ReportEntry {
id: string;
status: "finished" | "crashed" | "timeout";
wall_seconds: number;
}
function experimentOutcomes(
task: Task,
runDir: string,
process: Exclude<ProcessResult, { ending: "notStarted" }>,
experiments: DiagnosisExperiment[],
): ExperimentOutcome[] {
const path = join(runDir, DIAGNOSIS_REPORT);
const report = existsSync(path) ? (readJson(path) as ReportEntry[]) : [];
return experiments.map((e) => {
const entry = report.find((r) => r.id === e.id);
if (entry !== undefined) return { ending: entry.status, wallSeconds: entry.wall_seconds };
const failure = processFailure(task, process);
if (failure === null) throw new Error(`诊断运行正常退出,但 ${DIAGNOSIS_REPORT} 里没有实验 ${e.id}`);
return { ending: "notRun", reason: `没轮到:整次诊断运行${failure}` };
});
}
/** 没有答案可判的实验:不算数,辅助指标和假设专用指标都记测不到并附原因。 */
function unjudgedExperiment(
id: string,
ending: Exclude<DiagnosisEnding, "finished">,
wall: Measured,
reason: string,
hypothesisMetrics: HypothesisMetric[],
): DiagnosisExperimentJudged {
const metrics = {
...Object.fromEntries(FROM_VALIDATOR.map((m) => [m, unmeasured(`没有跑校验器:${reason}`)])),
...Object.fromEntries(hypothesisMetrics.map((m) => [m.id, unmeasured(`没有结果:${reason}`)])),
};
return { id, ending, wallSeconds: wall, verdict: "invalid", verdictReason: reason, score: unmeasured(reason), metrics, extra: { legalization: null } };
}
function judgedExperiments(
task: Task,
runDir: string,
judgeDir: string,
experiments: DiagnosisExperiment[],
outcomes: ExperimentOutcome[],
): DiagnosisJudged {
const eachSeconds = task.facts.run.wallSeconds / experiments.length;
return {
experiments: experiments.map((e, i): DiagnosisExperimentJudged => {
const outcome = outcomes[i]!;
if (outcome.ending === "notRun") return unjudgedExperiment(e.id, "notRun", unmeasured(outcome.reason), outcome.reason, e.metrics);
const wall = measured(outcome.wallSeconds);
if (outcome.ending === "timeout") {
return unjudgedExperiment(e.id, "timeout", wall, `超过分到的时间 ${eachSeconds} 秒,被结束`, e.metrics);
}
if (outcome.ending === "crashed") {
return unjudgedExperiment(e.id, "crashed", wall, `实验的进程以非零退出码结束(报错见这次运行的 stderr.log)`, e.metrics);
}
const dir = join(judgeDir, e.id);
const exitCode = Number(readFileSync(join(dir, EXIT_FILE), "utf8").trim());
const scored = scoreVerdict(readVerdict(dir, exitCode));
const hypothesis = readMetricsFile(join(runDir, ANSWERS, e.id, METRICS), `${ANSWERS}/${e.id}/${METRICS}`, e.metrics).hypothesis;
return {
id: e.id,
ending: "finished",
wallSeconds: wall,
verdict: scored.valid ? "valid" : "invalid",
verdictReason: scored.reason,
score: scored.score,
metrics: { ...scored.fromValidator, ...hypothesis },
extra: scored.extra,
};
}),
};
}
/** 逐个实验判分。校验器一个容器、按顺序对每个有答案的实验各跑一次,退出码各存一个文件。 */
const VALIDATE_EACH = `for id in "$@"; do
python -B ${VALIDATOR} --circles ${CIRCLES} --solution "$id/${SOLUTION}" --atol ${TOLERANCE} --legalize --verdict "$id/${VERDICT}"
echo $? > "$id/${EXIT_FILE}"
done`;
function prepareDiagnosisJudging(
task: Task,
runDir: string,
judgeDir: string,
imageDigest: string,
process: ProcessResult,
experiments: DiagnosisExperiment[],
): DiagnosisJudgingStep {
const run = started(process);
const outcomes = experimentOutcomes(task, runDir, run, experiments);
const finished = experiments.filter((_, i) => outcomes[i]!.ending === "finished");
if (finished.length === 0) return { status: "judged", result: judgedExperiments(task, runDir, judgeDir, experiments, outcomes) };
for (const e of finished) {
mkdirSync(join(judgeDir, e.id));
copyFileSync(join(runDir, ANSWERS, e.id, SOLUTION), join(judgeDir, e.id, SOLUTION));
}
copyFileSync(join(task.dir, VALIDATOR), join(judgeDir, VALIDATOR));
const command = ["sh", "-c", VALIDATE_EACH, "inforsi-validate-each", ...finished.map((e) => e.id)];
return { status: "validate", definition: definition(task, imageDigest, command, VALIDATOR_SECONDS * finished.length, {}) };
}
function judgeDiagnosis(
task: Task,
runDir: string,
judgeDir: string,
process: ProcessResult,
validator: ProcessResult | null,
experiments: DiagnosisExperiment[],
): DiagnosisJudged {
const run = started(process);
const outcomes = experimentOutcomes(task, runDir, run, experiments);
if (outcomes.some((o) => o.ending === "finished")) {
if (validator === null) throw new Error("有实验交出了答案,却没有校验器的运行结果");
requireExit("校验器", validator, [0]);
}
return judgedExperiments(task, runDir, judgeDir, experiments, outcomes);
}
export const circlePacking: TaskImplementation = {
metricDefinitions,
prepareRun,
formalRun,
lightCheck,
readLightCheck,
prepareJudging,
judge,
diagnosisUnversionedNames,
placeBaselineArtifact,
prepareDiagnosisRun,
diagnosisRun,
prepareDiagnosisJudging,
judgeDiagnosis,
};