import assert from "node:assert/strict"; import { test } from "node:test"; import { authorVsMerged, bitWidth, cellIndex, coverage, disagreement, placement, surprise, trackRecord, worldBits, type Mixture, } from "../src/rules/index.ts"; // 预期值来源写在每条测试旁边。比特的例子取 ε = 25(Δ = 0.25),格边界在二进制里精确。 function close(actual: number, expected: number, tol = 1e-12): void { assert.ok(Math.abs(actual - expected) <= tol, `实际 ${actual},预期 ${expected}`); } /** 很窄的范围 (y − 0.01, y, y + 0.01),连尾巴 [y − 0.02, y + 0.02]。 */ const narrow = (y: number) => ({ q1: y - 0.01, q5: y, q9: y + 0.01 }); const uniform = (lo: number, hi: number): Mixture => [ { weight: 1, knots: [ [lo, 0], [hi, 1], ], }, ]; test("6.9 格宽 Δ = ε/100,跟随 ε", () => { assert.equal(bitWidth(0.01), 0.01 / 100); assert.equal(bitWidth(25), 0.25); }); test("6.9 世界比特:从 b 起切;[0,1] 均匀切 4 格是 2 比特,错开 1/8 是 2.25 比特;整个落在一格里是 0", () => { // 手算:[0,1] 均匀、b = 0、Δ = 0.25:四格各 1/4,H = 2。 close(worldBits(uniform(0, 1), 0, 0.25), 2); // [0.125, 1.125]:五格 1/8、1/4、1/4、1/4、1/8,H = 2 × (1/8 × 3) + 3 × (1/4 × 2) = 0.75 + 1.5 = 2.25。 close(worldBits(uniform(0.125, 1.125), 0, 0.25), 2.25); // 同一分布、b = 0.125:切口对齐,四格各 1/4,又是 2 比特。说明格从 b 起切。 close(worldBits(uniform(0.125, 1.125), 0.125, 0.25), 2); // [0.3, 0.31] 整个在 [0.25, 0.5) 这一格里:把握 1,0 比特。 assert.equal(worldBits(uniform(0.3, 0.31), 0, 0.25), 0); }); test("6.9 惊讶 = −log₂ p;正好在格边界上的实测归右边那格;事前把握为 0 时给明确状态", () => { // 手算:[0,1] 均匀,实测 0.3 落在 [0.25, 0.5),p = 1/4,惊讶 2 比特。 assert.deepEqual(surprise(uniform(0, 1), 0, 0.25, 0.3), { cell: 1, prior: 0.25, bits: { status: "measured", value: 2 } }); assert.equal(surprise(uniform(0, 1), 0, 0.25, 0.25).cell, 1); assert.deepEqual(surprise(uniform(0, 1), 0, 0.25, 2), { cell: 8, prior: 0, bits: { status: "unmeasured", reason: "事前把握为 0,惊讶算不出" }, }); }); test("6.9 找实测所在格用和切格同一套边界:b + jΔ 正好落在第 j 格", () => { // b = 2.5、Δ = 1e-4:(b + 3Δ − b)/Δ 在浮点里可能是 2.999…,按边界核对后仍是 3。 for (let j = -5; j <= 50; j++) assert.equal(cellIndex(2.5, 1e-4, 2.5 + j * 1e-4), j); for (let j = -5; j <= 50; j++) assert.equal(cellIndex(2.635983, 1e-4, 2.635983 + j * 1e-4), j); }); test("6.9 分歧量:大家的范围完全一样时为 0;两个窄范围各在一格时为 1 比特", () => { const r = { q1: 0.2, q5: 0.5, q9: 0.9 }; close( disagreement( [ { id: "A", author: "A", range: r }, { id: "B", author: "B", range: r }, ], 0, 0.25, ), 0, ); // 手算:A 交 (0.09, 0.1, 0.11),连尾巴 [0.08, 0.12],整个在 [0, 0.25) 里;B 交 (0.59, 0.6, 0.61),整个在 [0.5, 0.75) 里。 // 各自 0 比特;平均后两格各一半,1 比特。 close( disagreement( [ { id: "A", author: "A", range: narrow(0.1) }, { id: "B", author: "B", range: narrow(0.6) }, ], 0, 0.25, ), 1, ); }); test("6.9 落点四段与覆盖率:边界归属;没有样本时覆盖率测不到", () => { const r = { q1: 1, q5: 2, q9: 3 }; assert.equal(placement(r, 0.5), "belowQ1"); assert.equal(placement(r, 1), "q1ToQ5"); assert.equal(placement(r, 2), "q5ToQ9"); assert.equal(placement(r, 3), "q5ToQ9"); assert.equal(placement(r, 3.5), "aboveQ9"); const c = coverage([0.5, 1, 2, 3, 3.5].map((value) => ({ range: r, value }))); assert.deepEqual(c.counts, { belowQ1: 1, q1ToQ5: 1, q5ToQ9: 2, aboveQ9: 1 }); assert.equal(c.covered, 3); assert.deepEqual(c.rate, { status: "measured", value: 0.6 }); assert.deepEqual(coverage([]).rate, { status: "unmeasured", reason: "没有样本" }); }); test("6.10 战绩:中位数偏高偏低、穿上下沿、包住,按人、按正向与消融分开", () => { const r = { q1: 1, q5: 2, q9: 3 }; const t = trackRecord([ { researcher: "B", phase: "forward", range: r, value: 2.5 }, { researcher: "A", phase: "ablation", range: r, value: 2 }, { researcher: "A", phase: "forward", range: r, value: 0.5 }, { researcher: "A", phase: "forward", range: r, value: 3.5 }, { researcher: "A", phase: "forward", range: r, value: 2.5 }, ]); assert.deepEqual( t.map((x) => [x.researcher, x.phase]), [ ["A", "forward"], ["A", "ablation"], ["B", "forward"], ], ); // A 正向:0.5 → 中位数偏高、穿下沿;3.5 → 偏低、穿上沿;2.5 → 偏低、包住。 assert.deepEqual(t[0]!.tally, { total: 3, medianHigh: 1, medianLow: 2, aboveTop: 1, belowBottom: 1, covered: 1, placements: { belowQ1: 1, q1ToQ5: 0, q5ToQ9: 1, aboveQ9: 1 }, }); // A 消融:实测正好等于 q5,不算偏高也不算偏低。 assert.equal(t[1]!.tally.medianHigh + t[1]!.tally.medianLow, 0); }); test("6.10 作者自评 vs 大家合并:作者一半按方向比合并一半好多少,正数是作者更乐观", () => { const item = { candidate: "c1", author: "A", authorRange: { q1: 2, q5: 2.5, q9: 3 }, mergedRange: { q1: 1, q5: 2, q9: 3 }, value: 2.25 }; assert.deepEqual(authorVsMerged([item], "maximize"), [ { candidate: "c1", author: "A", optimism: 0.5, authorPlacement: "q1ToQ5", mergedPlacement: "q5ToQ9" }, ]); assert.equal(authorVsMerged([item], "minimize")[0]!.optimism, -0.5); }); test("6.9 分歧量只量人与人之间:同一人几条解释先等权平均成他的折线", () => { // 手算:A、B 各交两条解释,都是 (0.09, 0.1, 0.11) 和 (0.59, 0.6, 0.61),各自整个落在 [0, 0.25)、[0.5, 0.75) 一格里。 // 每人的折线都是"两格各一半",各 1 比特;平均后仍是两格各一半,1 比特;分歧量 = 1 − 1 = 0。 // 若把每条解释各算比特(各 0 比特)再平均,会得出 1。 close( disagreement( [ { id: "e1", author: "A", range: narrow(0.1) }, { id: "e2", author: "A", range: narrow(0.6) }, { id: "e3", author: "B", range: narrow(0.1) }, { id: "e4", author: "B", range: narrow(0.6) }, ], 0, 0.25, ), 0, ); });