Files
InfoRSI/src/rules/forward.ts
T
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

182 lines
7.5 KiB
TypeScript
Raw 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 type { Direction, Range } from "../vocab/index.ts";
import { combinations, compareIds, epsilonFloor, improvement } from "./basics.ts";
import { forwardBatchSize } from "./budget.ts";
import {
forwardMergedRange,
forwardMixtures,
integrateUpperTail,
mixtureBelow,
toImprovement,
type Mixture,
} from "./distribution.ts";
import { seededSample } from "./random.ts";
// 方法.md 6.4 入池、选批、分数尺读数;6.5 采纳线;4.3 ⑫ 采纳。
/** 选批规则的统一输入:同一份输入可以换选批规则重算。 */
export interface BatchInput {
/** 本轮的全部候选(入池在规则里判);ranges 是每位研究者第二次预测的三个数(分数原值),一人一份,带研究者编号。 */
candidates: ReadonlyArray<{ id: string; ranges: ReadonlyArray<{ researcher: string; range: Range }> }>;
/** 本批基线分数 b。 */
baseline: number;
direction: Direction;
epsilon: number;
/** 开放预留 λ。 */
lambda: number;
batchCap: number;
/** 运行次数余额。 */
balance: number;
/** 本次决定的种子(decisionSeed)。批内最好期望规则不用它,照样记进记录。 */
seed: string;
}
/** 选批规则的统一输出,含 6.4"选批记录写入"的全部内容;规则名和版本由登记表补上。 */
export interface BatchDecision {
seed: string;
params: { epsilon: number; lambda: number; batchCap: number; balance: number };
/** 批量 k = min(每批上限, 余额)。 */
k: number;
/** 每个候选(按编号排):大家合起来的范围(分数原值)、"比 b 低 ε 以上"的把握、是否入池。 */
candidates: Array<{ id: string; merged: Range; lossProbability: number; inPool: boolean }>;
/** 入池的候选编号(按编号排)。 */
pool: string[];
/** 列出的每个组合及其批内最好期望 V(均匀随机规则不列组合,是空数组)。 */
combos: Array<{ ids: string[]; value: number }>;
/** 选中的组合(按编号排)。 */
chosen: string[];
/** V(S*)。 */
chosenValue: number;
/** E[max(0, max_{c∈池} Y_c)] = V(候选池)。 */
poolValue: number;
/** 分数尺读数 = V(候选池) − V(S*),只记录。 */
scaleReading: number;
}
/**
* 批内最好期望 V(S) = ∫_0^∞ [1 − ∏_{c∈S} F_c(t)] dt(6.4),F_c 是改进量上的合并分布,候选之间按互不相关,
* 低于 b 的部分按 0 算。空组合是 0。
*/
export function batchValue(imps: readonly Mixture[]): number {
return imps.length === 0 ? 0 : integrateUpperTail(imps, 0);
}
/** 入池(6.4):合并分布里"比 b 低 ε 以上"的把握 P(Y < −ε) 严格小于一半才入池;正好低 ε 不算"低 ε 以上"。 */
export function lossProbability(imp: Mixture, epsilon: number): number {
return mixtureBelow(imp, -epsilon);
}
/** 两条选批规则共用的部分:合并、入池、合并范围、批量、候选池的 V。 */
function assess(input: BatchInput) {
const sorted = [...input.candidates].sort((a, b) => compareIds(a.id, b.id));
const imps = forwardMixtures(sorted, input.lambda).map((m) => toImprovement(m, input.baseline, input.direction));
const candidates = sorted.map((c, i) => {
const loss = lossProbability(imps[i]!, input.epsilon);
return {
id: c.id,
merged: forwardMergedRange(imps[i]!, input.baseline, input.direction),
lossProbability: loss,
inPool: loss < 0.5,
};
});
const poolIdx = candidates.flatMap((c, i) => (c.inPool ? [i] : []));
return {
imps,
candidates,
poolIdx,
k: forwardBatchSize(input.batchCap, input.balance),
poolValue: batchValue(poolIdx.map((i) => imps[i]!)),
params: { epsilon: input.epsilon, lambda: input.lambda, batchCap: input.batchCap, balance: input.balance },
};
}
/**
* 选批规则"批内最好期望"(6.4):列出候选池里所有 min(k, 池里个数) 个候选的组合,取 V 最大的;
* V 完全相等(不带容差)时取编号字典序小的组合(组合按字典序列出,后来者严格更大才替换)。
*/
export function selectBestExpected(input: BatchInput): BatchDecision {
const a = assess(input);
const size = Math.min(a.k, a.poolIdx.length);
const picks = size === 0 ? [] : combinations(a.poolIdx.length, size);
const combos = picks.map((pick) => {
const idx = pick.map((p) => a.poolIdx[p]!);
return { ids: idx.map((i) => a.candidates[i]!.id), value: batchValue(idx.map((i) => a.imps[i]!)) };
});
// 池空或批量为 0 时一个组合都没有,S* 是空组合,V = 0。
let best = combos[0] ?? { ids: [], value: 0 };
for (const c of combos) if (c.value > best.value) best = c;
return {
seed: input.seed,
params: a.params,
k: a.k,
candidates: a.candidates,
pool: a.poolIdx.map((i) => a.candidates[i]!.id),
combos,
chosen: best.ids,
chosenValue: best.value,
poolValue: a.poolValue,
scaleReading: a.poolValue - best.value,
};
}
/**
* 选批规则"均匀随机"(第十一部分):从全部候选里(不看入池门槛)不放回地均匀抽 min(k, 候选数) 个。
* 入池、合并范围、分数尺读数照算照记;S* 是抽中的组合,可能含池外候选。
*/
export function selectUniformRandom(input: BatchInput): BatchDecision {
const a = assess(input);
const ids = a.candidates.map((c) => c.id);
const chosen = seededSample(ids, Math.min(a.k, ids.length), input.seed);
const chosenValue = batchValue(chosen.map((id) => a.imps[ids.indexOf(id)]!));
return {
seed: input.seed,
params: a.params,
k: a.k,
candidates: a.candidates,
pool: a.poolIdx.map((i) => a.candidates[i]!.id),
combos: [],
chosen,
chosenValue,
poolValue: a.poolValue,
scaleReading: a.poolValue - chosenValue,
};
}
/** 采纳线的输入(6.5):B 是当前基线分数,K* 是已知最好(没有时 null)。 */
export interface AdoptionLineInput {
baseline: number;
knownBest: number | null;
epsilon: number;
direction: Direction;
}
/**
* 采纳线(6.5):没有已知最好时是 ε;有时是 min(ε, (K* − B) ÷ 4)(按方向折算),结果低于 ε/100 时取 ε/100,
* B 已经超过 K*((K* − B) 为负)时同样取 ε/100。quarter 是 (K* − B) ÷ 4 的原值,没有已知最好时为 null。
*/
export function adoptionLine(input: AdoptionLineInput): { line: number; quarter: number | null } {
if (input.knownBest === null) return { line: input.epsilon, quarter: null };
const quarter = improvement(input.baseline, input.knownBest, input.direction) / 4;
return { line: Math.max(epsilonFloor(input.epsilon), Math.min(input.epsilon, quarter)), quarter };
}
/**
* 采纳(6.5、4.3 ⑫):只看实测分数。candidates 是有有效分数的正向候选;最好的那个(按方向,同分取编号小的)
* 比基线的改进严格大于采纳线才采纳。
*/
export function adopt(
input: AdoptionLineInput & { candidates: ReadonlyArray<{ id: string; score: number }> },
): {
line: number;
quarter: number | null;
best: { id: string; score: number; improvement: number } | null;
adopted: string | null;
} {
const { line, quarter } = adoptionLine(input);
let best: { id: string; score: number; improvement: number } | null = null;
for (const c of [...input.candidates].sort((x, y) => compareIds(x.id, y.id))) {
const gain = improvement(input.baseline, c.score, input.direction);
if (best === null || gain > best.improvement) best = { id: c.id, score: c.score, improvement: gain };
}
return { line, quarter, best, adopted: best !== null && best.improvement > line ? best.id : null };
}