import assert from "node:assert/strict"; import { test } from "node:test"; import { ablationCellMixture, authorShares, below, forwardMergedRange, forwardMixtures, knotsMean, mixtureBelow, mixtureQuantile, personKnots, reserveKnots, toImprovement, } from "../src/rules/index.ts"; // 预期值来源写在每条测试旁边。"手算"指按方法.md 的文字用纸笔算出,不调用实现里的公式。 function close(actual: number, expected: number, tol = 1e-12): void { assert.ok(Math.abs(actual - expected) <= tol, `实际 ${actual},预期 ${expected}`); } test("6.2 示例:交 2.44 / 2.52 / 2.62,低于 2.57 的把握是 0.70,尾巴铺在 2.36–2.44 与 2.62–2.72", () => { // 来源:方法.md 6.2 的示例原文。 const k = personKnots({ q1: 2.44, q5: 2.52, q9: 2.62 }); close(below(k, 2.57), 0.7); close(k[0]![0], 2.36); close(k[4]![0], 2.72); assert.equal(below(k, 2.36), 0); assert.equal(below(k, 2.73), 1); }); test("折线期望:对称范围的期望是一半那个数;不对称时手算", () => { // 手算:(0,1,2) 的五个点 (−1,0)(0,.1)(1,.5)(2,.9)(3,1),各段中点 × 把握: // .1×(−.5) + .4×.5 + .4×1.5 + .1×2.5 = −.05 + .2 + .6 + .25 = 1.0 close(knotsMean(personKnots({ q1: 0, q5: 1, q9: 2 })), 1); // (0,1,3):(−1,0)(0,.1)(1,.5)(3,.9)(5,1):.1×(−.5) + .4×.5 + .4×2 + .1×4 = −.05+.2+.8+.4 = 1.35 close(knotsMean(personKnots({ q1: 0, q5: 1, q9: 3 })), 1.35); }); test("6.3 开放预留:[L − W/2, U + W/2]", () => { // 手算:(0,1,2) 连尾巴 [−1, 3],(2,2.5,3) 连尾巴 [1.5, 3.5];L = −1,U = 3.5,W = 4.5,预留 [−3.25, 5.75]。 const r = reserveKnots([personKnots({ q1: 0, q5: 1, q9: 2 }), personKnots({ q1: 2, q5: 2.5, q9: 3 })]); assert.deepEqual(r, [ [-3.25, 0], [5.75, 1], ]); }); test("6.3 合并:(1 − λ) × 各人平均 + λ × 开放预留", () => { // 手算:两人 (0,1,2)、(2,3,4),λ = 0.1。L = −1,U = 5,W = 6,预留 [−4, 8]。 // 在 x = 2:第一人 0.9,第二人 0.1,平均 0.5;预留 (2 + 4)/12 = 0.5;合并 0.9×0.5 + 0.1×0.5 = 0.5。 // 在 x = 0:第一人 0.1,第二人 0(尾巴从 1 开始);预留 4/12;合并 0.9×0.05 + 0.1/3 = 0.045 + 0.0333… const [m] = forwardMixtures( [ { ranges: [ { researcher: "r1", range: { q1: 0, q5: 1, q9: 2 } }, { researcher: "r2", range: { q1: 2, q5: 3, q9: 4 } }, ], }, ], 0.1, ); close(mixtureBelow(m!, 2), 0.5); close(mixtureBelow(m!, 0), 0.045 + 0.1 / 3); }); test("6.3 正向的开放预留由同一次选批里全部候选、全部人的折线一起定", () => { // 手算:候选甲 (0,1,2),候选乙 (9,10,11) 连尾巴 [8, 12]。L = −1,U = 12,W = 13,预留 [−7.5, 18.5],两个候选共用。 const ms = forwardMixtures( [ { ranges: [{ researcher: "r1", range: { q1: 0, q5: 1, q9: 2 } }] }, { ranges: [{ researcher: "r1", range: { q1: 9, q5: 10, q9: 11 } }] }, ], 0.5, ); for (const m of ms) { assert.deepEqual(m[m.length - 1]!.knots, [ [-7.5, 0], [18.5, 1], ]); assert.equal(m[m.length - 1]!.weight, 0.5); } }); test("6.3 同一个人交了几条解释时,他那一份在几条之间平分", () => { // 手算:A 两条、B 一条:A 的每条 1/4,B 的那条 1/2。 assert.deepEqual(authorShares(["A", "A", "B"]), [0.25, 0.25, 0.5]); // λ = 0.1 时成分权重 0.9×0.25、0.9×0.25、0.9×0.5、0.1。 const r = { q1: 1, q5: 2, q9: 3 }; const m = ablationCellMixture( [ { id: "e1", author: "A", range: r }, { id: "e2", author: "A", range: r }, { id: "e3", author: "B", range: r }, ], 0.1, ); assert.deepEqual( m.map((c) => c.weight), [0.9 * 0.25, 0.9 * 0.25, 0.9 * 0.5, 0.1], ); }); test("合并分布的一成、一半、九成(广义逆)", () => { // 手算:一人 (0,1,2),λ = 0.5,预留 [−3, 5]。F(x) = 0.5 F₁(x) + 0.5 (x + 3)/8。 // 一成:在 [−3, −1] 上 F₁ = 0,(x + 3)/16 = 0.1 → x = −1.4(在 [−1, 0] 上解出 −1.22 < −1,不在段内)。 // 对称性:一半是 1,九成是 3.4。 const [m] = forwardMixtures([{ ranges: [{ researcher: "r1", range: { q1: 0, q5: 1, q9: 2 } }] }], 0.5); close(mixtureQuantile(m!, 0.1), -1.4); close(mixtureQuantile(m!, 0.5), 1); close(mixtureQuantile(m!, 0.9), 3.4); }); test("6.1 越小越好:换成改进量后一成和九成对调,合并范围换回原值后与原范围相同", () => { // 手算:b = 1,一人交 (1, 1.25, 1.5),λ = 0。改进量 Y = 1 − 分数:(−0.5, −0.25, 0) 依次是一成、一半、九成。 const [m] = forwardMixtures([{ ranges: [{ researcher: "r1", range: { q1: 1, q5: 1.25, q9: 1.5 } }] }], 0); const imp = toImprovement(m!, 1, "minimize"); // 反向时把握换成 1 − 原把握,1 − 0.9 在浮点里差末位,用容差比。 close(mixtureBelow(imp, -0.5), 0.1); close(mixtureBelow(imp, -0.25), 0.5); close(mixtureBelow(imp, 0), 0.9); const merged = forwardMergedRange(imp, 1, "minimize"); close(merged.q1, 1); close(merged.q5, 1.25); close(merged.q9, 1.5); // 越大越好:b = 1,交 (0.5, 0.75, 1),Y = 分数 − 1。 const [n] = forwardMixtures([{ ranges: [{ researcher: "r1", range: { q1: 0.5, q5: 0.75, q9: 1 } }] }], 0); const impMax = toImprovement(n!, 1, "maximize"); assert.equal(mixtureBelow(impMax, -0.25), 0.5); assert.deepEqual(forwardMergedRange(impMax, 1, "maximize"), { q1: 0.5, q5: 0.75, q9: 1 }); });