import assert from "node:assert/strict"; import { test } from "node:test"; import { ablationModel, assignEntries, authorVsMerged, chooseBet, compareIds, disagreement, evaluateCombos, forwardMixtures, pickByNet, selectBestExpected, selectByValue, settleAblation, topCombo, worldBits, type AblationCell, type AblationExplanation, type CellRuleInput, } from "../src/rules/index.ts"; import { canonical } from "../src/vocab/index.ts"; import { loadFixture, range } from "./rules-fixtures-helpers.ts"; // 审阅提出的四个问题各一组复现测试: // 一、结果不随传入次序变(6.7"重算结果逐位相同"):求和次序由编号定。打乱输入后用 canonical 比整个返回值,逐位相同。 // 二、编号按自然序比("编号最小"是数字意义上的大小)。 // 三、入口分配与数组次序无关。 // 四、选格不做容差接力(6.7"选格"第 2 条)。 /** 真实第 1 轮消融(三格、三人各两条解释),各条解释和各格交叉着出现,求和次序一变末位就会变。 */ const R1 = loadFixture("ablation.json").models!.r1real!.input; const EXPLANATIONS: AblationExplanation[] = R1.explanations.map((e: any) => ({ id: e.id, author: e.author, cells: Object.entries(e.cells).map(([factor, r]) => ({ factor, range: range(r as any) })), })); const CELLS: AblationCell[] = [ { factor: "p1", outcome: { kind: "valid", score: 2.250011787062861 } }, { factor: "p2", outcome: { kind: "insufficient" } }, { factor: "p3", outcome: { kind: "valid", score: 2.4389662413796342 } }, ]; const BASE = { baseline: R1.B as number, full: R1.full as number, direction: "maximize" as const, knownBest: R1.knownBest as number, epsilon: 0.01, lambda: 0.1, }; /** 几种打乱:倒序、轮转、奇偶交错,以及解释内各格也倒序。 */ function shuffles(xs: readonly T[]): T[][] { const rot = [...xs.slice(1), ...xs.slice(0, 1)]; const evenOdd = [...xs.filter((_, i) => i % 2 === 0), ...xs.filter((_, i) => i % 2 === 1)]; return [[...xs].reverse(), rot, evenOdd]; } const explanationShuffles = (): AblationExplanation[][] => shuffles(EXPLANATIONS).map((es) => es.map((e) => ({ ...e, cells: [...e.cells].reverse() }))); test("一、消融结算:打乱解释和格的次序,结果逐位相同", () => { const factors = ["p1", "p2", "p3"]; const ref = canonical(settleAblation({ ...BASE, factors, cells: CELLS, explanations: EXPLANATIONS })); for (const es of explanationShuffles()) { for (const cs of shuffles(CELLS)) { for (const fs of shuffles(factors)) { assert.equal(canonical(settleAblation({ ...BASE, factors: fs, cells: cs, explanations: es })), ref); } } } }); test("一、信息价值与选格:打乱解释和因子的次序,组合表和选格决定逐位相同", () => { const input = (es: AblationExplanation[], factors: string[]): CellRuleInput => ({ ...BASE, factors, explanations: es, balance: 4, batchCap: 2, u: 0.5, decay: 0.5, seed: "seed", }); const factors = ["p1", "p2", "p3"]; const refModel = ablationModel(input(EXPLANATIONS, factors)); const refCombos = canonical(evaluateCombos(refModel, input(EXPLANATIONS, factors))); const refDecision = canonical(selectByValue(input(EXPLANATIONS, factors))); for (const es of explanationShuffles()) { for (const fs of shuffles(factors)) { const i = input(es, fs); assert.equal(canonical(evaluateCombos(ablationModel(i), i)), refCombos); assert.equal(canonical(selectByValue(i)), refDecision); } } }); test("一、押的因子:打乱解释、因子和格的次序,结果逐位相同", () => { const factors = ["p1", "p2", "p3"]; const ref = canonical(chooseBet({ ...BASE, factors, explanations: EXPLANATIONS, cells: CELLS })); for (const es of explanationShuffles()) { for (const fs of shuffles(factors)) { for (const cs of shuffles(CELLS)) { assert.equal(canonical(chooseBet({ ...BASE, factors: fs, explanations: es, cells: cs })), ref); } } } }); test("一、分歧量:打乱条目次序,结果逐位相同", () => { for (const f of ["p1", "p2", "p3"]) { const entries = EXPLANATIONS.map((e) => ({ id: e.id, author: e.author, range: e.cells.find((c) => c.factor === f)!.range, })); const ref = disagreement(entries, BASE.baseline, 1e-4); for (const es of shuffles(entries)) assert.equal(disagreement(es, BASE.baseline, 1e-4), ref, `${f}`); } }); /** 正向三位研究者(researcher-2、researcher-10、researcher-3,故意让字符串序和自然序不同)对三个候选的范围。 */ const FORWARD_PEOPLE = ["researcher-2", "researcher-10", "researcher-3"]; const forwardRanges = (a: number[][]) => a.map(([q1, q5, q9], i) => ({ researcher: FORWARD_PEOPLE[i]!, range: { q1: q1!, q5: q5!, q9: q9! } })); const FORWARD = [ { id: "c1", ranges: forwardRanges([[2.0, 2.45, 2.6], [2.2, 2.5, 2.62], [2.2, 2.5, 2.63]]) }, { id: "c2", ranges: forwardRanges([[1.85, 2.35, 2.6], [1.6, 2.35, 2.6], [1.8, 2.4, 2.62]]) }, { id: "c10", ranges: forwardRanges([[2.3, 2.5, 2.62], [2.42, 2.52, 2.6], [2.1, 2.5, 2.6]]) }, ]; test("一、正向:打乱候选的次序和每个候选里各人范围的次序,选批结果、合并分布的比特逐位相同", () => { const run = (cs: typeof FORWARD) => canonical( selectBestExpected({ candidates: cs, baseline: BASE.baseline, direction: "maximize", epsilon: 0.01, lambda: 0.1, batchCap: 2, balance: 10, seed: "seed", }), ); const bits = (cs: typeof FORWARD) => forwardMixtures(cs, 0.1).map((m) => worldBits(m, BASE.baseline, 1e-4)); const ref = run(FORWARD); const refBits = bits(FORWARD); for (const cs of shuffles(FORWARD)) { for (const k of [0, 1, 2]) { // 每个候选里各人范围也打乱(三种打乱轮着用)。 const mixed = cs.map((c) => ({ ...c, ranges: shuffles(c.ranges)[k]! })); assert.equal(run(mixed), ref); const byId = new Map(mixed.map((c, i) => [c.id, bits(mixed)[i]!])); assert.deepEqual(FORWARD.map((c) => byId.get(c.id)), refBits); } } }); test("一、正向分歧量:打乱各人次序,结果逐位相同", () => { for (const c of FORWARD) { const entries = c.ranges.map((r) => ({ id: r.researcher, author: r.researcher, range: r.range })); const ref = disagreement(entries, BASE.baseline, 1e-4); for (const es of shuffles(entries)) assert.equal(disagreement(es, BASE.baseline, 1e-4), ref, c.id); } }); test("二、编号按自然序:c2 < c10、researcher-2 < researcher-10、p2 < p10", () => { assert.ok(compareIds("c2", "c10") < 0); assert.ok(compareIds("researcher-2", "researcher-10") < 0); assert.ok(compareIds("p2", "p10") < 0); assert.ok(compareIds("r1-e2-1", "r1-e10-1") < 0); assert.ok(compareIds("c9", "d1") < 0, "非数字段按字符串比"); assert.ok(compareIds("c", "c1") < 0, "段先用完的排前面"); // 数字段不转成浮点数:超出 2^53 的长编号也按数值比。 assert.ok(compareIds("c9007199254740993", "c9007199254740992") > 0); assert.ok(compareIds("c010", "c9") > 0, "去掉前导零后比位数"); assert.ok(compareIds("p01", "p1") !== 0, "数值相同、写法不同的编号不相等"); assert.equal(compareIds("c10", "c10"), 0); assert.deepEqual(["c10", "c2", "c1", "c11"].sort(compareIds), ["c1", "c2", "c10", "c11"]); }); test("二、批内最好期望完全相等时,c2 排在 c10 前面", () => { const r = [{ researcher: "r1", range: { q1: 2.4, q5: 2.5, q9: 2.6 } }]; const d = selectBestExpected({ candidates: [ { id: "c10", ranges: r }, { id: "c2", ranges: r }, ], baseline: 2.45, direction: "maximize", epsilon: 0.01, lambda: 0.1, batchCap: 1, balance: 10, seed: "seed", }); assert.equal(d.combos[0]!.value, d.combos[1]!.value); assert.deepEqual(d.chosen, ["c2"]); }); test("二、三、researcher-2 与 researcher-10 争同一个入口:归编号小的 researcher-2,与数组次序无关", () => { const researchers = [ { id: "researcher-10", choices: ["e1"] }, { id: "researcher-2", choices: ["e1"] }, { id: "researcher-3", choices: ["e10"] }, ]; const entries = [ { id: "e10", author: "researcher-2" }, { id: "e1", author: "researcher-3" }, { id: "e2", author: "researcher-3" }, ]; const expected = [ { researcher: "researcher-2", entry: "e1", rank: 1 }, { researcher: "researcher-3", entry: "e10", rank: 1 }, { researcher: "researcher-10", entry: "e2", rank: null }, ]; assert.deepEqual(assignEntries({ researchers, entries }), expected); for (const rs of shuffles(researchers)) { for (const es of shuffles(entries)) assert.deepEqual(assignEntries({ researchers: rs, entries: es }), expected); } }); test("四、选格不做容差接力:{p1}=1、{p2}=1+0.9e-12、{p1,p2}=1+1.8e-12 选 {p2}", () => { const combos = [ { factors: ["p1"], net: 1 }, { factors: ["p2"], net: 1 + 0.9e-12 }, { factors: ["p1", "p2"], net: 1 + 1.8e-12 }, ]; assert.deepEqual(pickByNet(combos), ["p2"]); // 与列出的次序无关。 assert.deepEqual(pickByNet([...combos].reverse()), ["p2"]); // "含这一格的净值最大组合"同一条规则:含 p1 的里最大 1+1.8e-12,窗口内只有 {p1,p2}。 assert.deepEqual(topCombo(combos.filter((c) => c.factors.includes("p1")))!.factors, ["p1", "p2"]); // 窗口内格数相同时按因子编号比,p2 < p10。 assert.deepEqual( pickByNet([ { factors: ["p10"], net: 1 + 0.5e-12 }, { factors: ["p2"], net: 1 }, ]), ["p2"], ); // 最大净值不大于 1e-12 时一格不测。 assert.deepEqual(pickByNet([{ factors: ["p1"], net: 1e-12 }]), []); }); test("四、选格先只留净值 > 1e-12 的组合再求最大:{p1}=0.6e-12、{p1,p2}=1.5e-12 选 {p1,p2}", () => { // 第二轮审阅的反例。先在全部组合里求最大再开窗口,{p1} 落在窗口里、格数少,会被选中,但它的净值不 > 1e-12。 const combos = [ { factors: ["p1"], net: 0.6e-12 }, { factors: ["p1", "p2"], net: 1.5e-12 }, ]; assert.deepEqual(pickByNet(combos), ["p1", "p2"]); // "含这一格的最好组合"只是记录,不受 > 1e-12 限制:只在 {p1} 一个组合里挑,就是它。 assert.deepEqual(topCombo([{ factors: ["p1"], net: 0.6e-12 }])!.factors, ["p1"]); }); test("一、作者自评 vs 合并:输出按候选编号的自然序排,不跟着输入次序", () => { // 手算(越大越好):c10 作者一半 2.5、合并一半 2.4,乐观 0.1;c2 作者一半 2.3、合并一半 2.4,乐观 −0.1。 const r = (q5: number) => ({ q1: q5 - 0.1, q5, q9: q5 + 0.1 }); const items = [ { candidate: "c10", author: "r1", authorRange: r(2.5), mergedRange: r(2.4), value: 2.45 }, { candidate: "c2", author: "r2", authorRange: r(2.3), mergedRange: r(2.4), value: 2.45 }, ]; for (const xs of [items, [...items].reverse()]) { const got = authorVsMerged(xs, "maximize"); assert.deepEqual(got.map((g) => g.candidate), ["c2", "c10"]); assert.ok(Math.abs(got[0]!.optimism - -0.1) <= 1e-12 && Math.abs(got[1]!.optimism - 0.1) <= 1e-12); } });