Files
InfoRSI/tests/rules-distribution.test.ts
iomgaa 60c21ae346 feat: 研究规则 rules 模块(任务书 02)
rules 是纯计算,src/rules 共 1761 行:方法.md 第六部分的全部公式、第七部分的余额与扣次、4.2 的入口分配,以及种子与规则登记表。

预期值有三个来源:方法.md 的手算例;tests/fixtures/rules/ 下不看实现写的 Python 脚本;参考项目 calc15、calc14b 里与现行方法一致的部分。审阅做了两轮,每轮都换一个全新上下文的子 Agent,提出的问题都已修复或写明不修的理由。

用户确认的约定(原话"1–8 都同意"):入池、采纳、消融中发现都用严格不等号;成本用 max(0, ĝ₀);没测的格附"含它的最好组合"的净值;战绩在分数原值上判。三个数改为严格递增后,删掉了尖峰的全部专门处理(bfb96ba)。vocab 里 Range 的说明同步改为严格递增。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:50:21 -04:00

134 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
});