852600df02
消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。 eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
557 lines
24 KiB
TypeScript
557 lines
24 KiB
TypeScript
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,
|
||
};
|