245 lines
8.9 KiB
TypeScript
245 lines
8.9 KiB
TypeScript
import type { Content } from "../../ledger/index.ts";
|
|
import {
|
|
Board,
|
|
cellId,
|
|
CELL_NOT_RUN,
|
|
explanationId,
|
|
protocolMetrics,
|
|
splitCell,
|
|
validScore,
|
|
} from "../../materials/index.ts";
|
|
import {
|
|
chooseBet,
|
|
decisionSeed,
|
|
runBalance,
|
|
runCellRule,
|
|
settleAblation,
|
|
sharedSettings,
|
|
type AblationExplanation,
|
|
type AblationModelInput,
|
|
type CellOutcome,
|
|
type CellRuleInput,
|
|
type CellRuleName,
|
|
type SettleInput,
|
|
} from "../../rules/index.ts";
|
|
import type { HypothesisMetric, Json } from "../../vocab/index.ts";
|
|
import { formalRun, type RunOutcome } from "../formal-run.ts";
|
|
import { ablationOf, aliveCells, lastAblationImpl } from "../next-step.ts";
|
|
import { SEED_VERSIONS } from "./round.ts";
|
|
import type { Study } from "../study.ts";
|
|
|
|
// ⑮ 选格、⑯ 消融运行、⑰ 结算与押的因子、⑱ 轮末解封(任务书 17 第 2.2 节)。选格、结算和押的因子各写一条记录
|
|
// (结算和押的因子同一个事务),写下之前被打断就从账本重新算一遍;写下之后恢复时照原样用,不重算(方法.md 第九部分)。
|
|
|
|
/**
|
|
* ⑮ 选格(方法.md 6.7):基线、完整分数、方向、已知最好、ε、λ、因子、各条解释,加余额、每批上限、u、衰减和种子,
|
|
* 交给步骤清单里 selectCells 那一步的规则,写一条选格决定。种子用决定的版本 2(选批已用版本 1,方法.md 第九部分)。
|
|
*/
|
|
export function selectCells(s: Study, round: number): void {
|
|
const board = new Board(s.ledger.records());
|
|
const { config, steps } = s.frozen;
|
|
const seed = decisionSeed({
|
|
studyId: config.studyId,
|
|
round,
|
|
version: SEED_VERSIONS.cells,
|
|
});
|
|
const input: CellRuleInput = {
|
|
...modelInput(board, s, round),
|
|
balance: runBalance(config.runs, board.runStates()).balance,
|
|
batchCap: config.batchLimit,
|
|
u: config.flow.knowledgeUse,
|
|
decay: config.flow.decay,
|
|
seed,
|
|
};
|
|
const rule = steps.round.find((x) => x.id === "selectCells")!.rule!
|
|
.name as CellRuleName;
|
|
const { rule: used, ...output } = runCellRule(rule, input);
|
|
// SAFETY: rules 是纯计算,输入输出都是纯 Json 值;账本在写入时校验 Json。
|
|
s.ledger.append("decision", {
|
|
decision: "cells",
|
|
round,
|
|
rule: used.name,
|
|
ruleVersion: used.version,
|
|
params: output.params,
|
|
seed,
|
|
input: input as unknown as Json,
|
|
output: output as unknown as Json,
|
|
});
|
|
}
|
|
|
|
/** ⑯ 消融运行:用核验过的这一格快照,复用正式运行的登记、扣次、判分入口。完整代码和基线的分数取本批已有的运行结果,不重跑。 */
|
|
export function ablationRun(
|
|
s: Study,
|
|
round: number,
|
|
cell: string,
|
|
run: string,
|
|
): Promise<RunOutcome> {
|
|
const board = new Board(s.ledger.records());
|
|
return formalRun(
|
|
s,
|
|
{ run, purpose: "ablation", round, object: cell },
|
|
board.code(cell).version,
|
|
ablationMetrics(board, splitCell(cell).candidate),
|
|
);
|
|
}
|
|
|
|
/** 消融格正式运行登记的指标:照被采纳候选的冻结协议取;消融格里假设专用指标缺失一律只记缺失、不作废这一格(方法.md 第五部分"指标")。 */
|
|
export function ablationMetrics(
|
|
board: Board,
|
|
candidate: string,
|
|
): HypothesisMetric[] {
|
|
return protocolMetrics(board, board.protocol(candidate)).map((m) => ({
|
|
...m,
|
|
missing: "recordMissing",
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* ⑰ 结算(方法.md 4.3 ⑰、6.6):选中的格全部有结果、"跑不起来"或"不跑"事件后,照 SettleInput 拼输入,
|
|
* 写一条结算记录;押的因子(6.8)在同一个事务里写成种类 bet 的决定。
|
|
* "消融中发现"在 settleAblation 的输出里:materials 的 Board.baselineOf 据此把那一格的代码当成下一轮的基线,不另写采纳记录。
|
|
*/
|
|
export function settle(s: Study, round: number): void {
|
|
const board = new Board(s.ledger.records());
|
|
const { candidate, keys } = ablationOf(board, round)!;
|
|
const events = s.ledger
|
|
.records()
|
|
.flatMap((r) => (r.kind === "event" ? [r.content] : []));
|
|
const model = modelInput(board, s, round);
|
|
const input: SettleInput = {
|
|
...model,
|
|
cells: keys.map((key) => ({
|
|
factor: key,
|
|
outcome: outcomeOf(board, events, round, candidate, key),
|
|
})),
|
|
};
|
|
const output = settleAblation(input);
|
|
const betInput = { ...model, cells: input.cells };
|
|
const bet = chooseBet(betInput);
|
|
// SAFETY: rules 是纯计算,结算和决定的输入输出都是纯 Json 值;账本在写入时校验 Json。
|
|
s.ledger.transaction(() => {
|
|
s.ledger.append("settlement", {
|
|
round,
|
|
result: { candidate, input, output } as unknown as Json,
|
|
});
|
|
s.ledger.append("decision", {
|
|
decision: "bet",
|
|
round,
|
|
rule: "chooseBet",
|
|
ruleVersion: 1,
|
|
params: {
|
|
direction: model.direction,
|
|
epsilon: model.epsilon,
|
|
knownBest: model.knownBest,
|
|
lambda: model.lambda,
|
|
},
|
|
seed: null,
|
|
input: betInput as unknown as Json,
|
|
output: bet as unknown as Json,
|
|
});
|
|
});
|
|
}
|
|
|
|
/** 一格的结局(方法.md 6.6"一格的几种结局"):没测、跑不起来、证据不足、一起关、有效分数。 */
|
|
function outcomeOf(
|
|
board: Board,
|
|
events: readonly Content<"event">[],
|
|
round: number,
|
|
candidate: string,
|
|
key: string,
|
|
): CellOutcome {
|
|
if (!board.decision("cells", round)!.output.chosen.includes(key))
|
|
return { kind: "untested" };
|
|
const cell = cellId(candidate, key);
|
|
const impl = lastAblationImpl(board, cell)!;
|
|
if (impl.content.cannotRun !== null) return { kind: "cannotRun" };
|
|
if (events.some((e) => e.event === CELL_NOT_RUN && e.objects.includes(cell)))
|
|
return { kind: "insufficient" };
|
|
const off = [key, ...impl.content.alsoOff];
|
|
// 几格实际关掉的因子完全相同时,沿用同设置第一格的运行结果(不重跑,方法.md 6.6"一起关")。
|
|
const alive = aliveCells(board, events, round);
|
|
const sameAs = new Map(
|
|
sharedSettings(
|
|
alive.map((x) => ({ factor: splitCell(x.cell).key, off: x.off })),
|
|
).map((x) => [x.factor, x.sameAs]),
|
|
);
|
|
const first = sameAs.get(key);
|
|
const ranCell =
|
|
first === null || first === undefined ? cell : cellId(candidate, first);
|
|
const run = board
|
|
.runs()
|
|
.filter(
|
|
(r) =>
|
|
r.plan.purpose === "ablation" &&
|
|
r.plan.round === round &&
|
|
r.plan.object === ranCell,
|
|
)
|
|
.at(-1)!;
|
|
const score = validScore(run.result!.judged);
|
|
if (off.length > 1) return { kind: "joint", off, score };
|
|
return score === null ? { kind: "insufficient" } : { kind: "valid", score };
|
|
}
|
|
|
|
/**
|
|
* ⑱ 轮末解封(方法.md 4.3 ⑱):本轮选批选中的每个候选(跑过的、入选了却没跑成的)解封作者自评,
|
|
* 全部写在同一个事务里。只在收尾阶段关闭之后走到这里("下一步做什么"保证),提前写会泄露。
|
|
* "入选了却没跑成的候选离开候选池"不写记录:Board.carried 只留从没进过任何一批的候选。
|
|
*/
|
|
export function unsealChosen(s: Study, round: number): void {
|
|
const board = new Board(s.ledger.records());
|
|
const records = s.ledger.records();
|
|
const chosen = board.decision("batch", round)!.output.chosen;
|
|
s.ledger.transaction(() => {
|
|
for (const candidate of chosen) {
|
|
if (
|
|
records.some(
|
|
(r) => r.kind === "unseal" && r.content.candidate === candidate,
|
|
)
|
|
)
|
|
continue;
|
|
const seals = board.proposalsOf(candidate).map((id) => {
|
|
const seq = board.proposal(id).seq;
|
|
const seal = records.find(
|
|
(r) => r.kind === "seal" && r.content.proposal === seq,
|
|
);
|
|
if (seal === undefined)
|
|
throw new Error(
|
|
`提案 ${id}(记录 ${seq})没有封存记录:提案的作者自评在写入提案时一并封存`,
|
|
);
|
|
return seal.seq;
|
|
});
|
|
s.ledger.append("unseal", { candidate, seals });
|
|
}
|
|
});
|
|
}
|
|
|
|
/** 成分模型的输入(方法.md 6.7):选格、结算和押的因子共用。完整代码和本批基线的分数取本批已有的运行结果。 */
|
|
function modelInput(board: Board, s: Study, round: number): AblationModelInput {
|
|
const { candidate, keys } = ablationOf(board, round)!;
|
|
const { config, facts } = s.frozen;
|
|
return {
|
|
baseline: board.baselineScore(board.baselineOf(round)),
|
|
full: board.baselineScore(candidate),
|
|
direction: facts.finalMetric.direction,
|
|
knownBest: facts.knownBest,
|
|
epsilon: facts.epsilon,
|
|
lambda: config.flow.openReserve,
|
|
factors: keys,
|
|
explanations: explanationsOf(board, round),
|
|
};
|
|
}
|
|
|
|
/** 这一轮已冻结的各条解释:作者的第 i 条(materials 的 explanationId),每条对每一格给的三个数。 */
|
|
function explanationsOf(board: Board, round: number): AblationExplanation[] {
|
|
return board
|
|
.published("explanation")
|
|
.filter((x) => x.round === round)
|
|
.flatMap((x) =>
|
|
x.content.explanations.map((e, i) => ({
|
|
id: explanationId(round, x.author, i + 1),
|
|
author: x.author,
|
|
cells: e.cells!,
|
|
})),
|
|
);
|
|
}
|