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>
This commit is contained in:
2026-10-02 01:50:21 -04:00
parent bfb96ba7d7
commit 60c21ae346
37 changed files with 14067 additions and 3 deletions
+282
View File
@@ -0,0 +1,282 @@
{
"source": "research-wiki/设计-15-计算/calc15.py(14-real-1 账本副本)",
"input": {
"baseline": 0.9597642169962064,
"full": 2.6276819772628,
"knownBest": 2.635,
"offGrid": 2.5989997592071172,
"offSolverLegalized": 1.7602769492805097,
"explanations": [
{
"id": "e1",
"author": "researcher-1",
"cells": [
[
1.4,
1.75,
2.1
],
[
1.9,
2.05,
2.18
]
]
},
{
"id": "e2",
"author": "researcher-1",
"cells": [
[
1.9,
2.3,
2.55
],
[
1.8,
1.95,
2.1
]
]
},
{
"id": "e3",
"author": "researcher-1",
"cells": [
[
1.65,
2.05,
2.4
],
[
1.8,
1.98,
2.15
]
]
},
{
"id": "e4",
"author": "researcher-2",
"cells": [
[
2.3,
2.45,
2.58
],
[
1.85,
2.02,
2.18
]
]
},
{
"id": "e5",
"author": "researcher-2",
"cells": [
[
1.5,
1.95,
2.35
],
[
1.9,
2.06,
2.2
]
]
},
{
"id": "e6",
"author": "researcher-2",
"cells": [
[
1.8,
2.05,
2.3
],
[
1.85,
2,
2.15
]
]
},
{
"id": "e7",
"author": "researcher-3",
"cells": [
[
1.9,
2.15,
2.4
],
[
1.95,
2.1,
2.2
]
]
},
{
"id": "e8",
"author": "researcher-3",
"cells": [
[
2.25,
2.45,
2.55
],
[
1.7,
1.9,
2.05
]
]
},
{
"id": "e9",
"author": "researcher-3",
"cells": [
[
1.7,
1.95,
2.2
],
[
1.85,
2,
2.15
]
]
}
]
},
"G": 1.6679177602665936,
"h": 0.007318022737199836,
"supportOffGrid": 0.017196422233131125,
"supportOffSolver": 0.5200526360746033,
"priorSupport2": [
0.3226369969085209,
0.3808832739818412
],
"priorSupport3": [
0.3226369969085209,
0.3808832739818412,
0.34891227321051016
],
"voi2": {
"1": 0.0005820672232162948,
"2": 0.0002053645817553188,
"1+2": 0.0006313506372333821
},
"voi2GridAll": 0.0006313460547053311,
"voi3": {
"1": 0.0005820672232162948,
"2": 0.00023468479202717538,
"3": 0.00037866484827430574,
"1+2": 0.0007066366436911577,
"1+3": 0.0008066824756162668,
"2+3": 0.0005046379680289518,
"1+2+3": 0.0008773803854093963
},
"voi3GridAll": 0.0008771372131003269,
"g0": 0.0027873124592182287,
"opp": {
"1": 0.00046455207653637166,
"2": 0.0007432833224581948
},
"hand": {
"reservePrior": 0.1,
"reservePosterior": 0.08426511100308286,
"explanationsPosterior": 0.9157348889969171,
"gainOffGrid": 0.00012584380890054235,
"solverGainPrior": 0.0027873124592182287,
"solverGainPosterior": 0.002893886609905632
},
"gridCap": {
"1": 4000000,
"2": 2000,
"3": 158,
"4": 44
},
"cellMiniSegments": {
"k1": [
620,
600
],
"k2": [
620,
600
],
"k3": [
155,
150,
126
]
},
"labels": {
"[0.95, 0.93, 0.91]": "缺一不可",
"[0.95, 0.93, 0.9]": "互补",
"[0.6, 0.4, 0.3]": "互补",
"[0.6, 0.4, 0.1]": "各管各",
"[0.5, 0.3, 0.1]": "各管各",
"[0.5, null, 0.3]": "不全,不贴标签"
},
"label2": "替代",
"choose": {
"2cells_bal5": {
"chosen": [
1
],
"net": {
"none": 0.0,
"1": 0.00043655041741222107,
"2": 0.0001540234363164891,
"1+2": -0.0002697703445331582
}
},
"2cells_bal7": {
"chosen": [
1,
2
],
"net": {
"none": 0.0,
"1": 0.00043655041741222107,
"2": 0.0001540234363164891,
"1+2": 0.00047351297792503657
}
},
"2cells_bal10": {
"chosen": [
1,
2
],
"net": {
"none": 0.0,
"1": 0.00043655041741222107,
"2": 0.0001540234363164891,
"1+2": 0.00047351297792503657
}
},
"3cells_bal10": {
"chosen": [
1,
2,
3
],
"net": {
"none": 0.0,
"1": 0.00043655041741222107,
"2": 0.00017601359402038153,
"3": 0.0002839986362057293,
"1+2": 0.0005299774827683683,
"1+3": 0.0006050118567122001,
"2+3": 0.0003784784760217138,
"1+2+3": 0.0006580352890570472
}
}
}
}
+3862
View File
File diff suppressed because it is too large Load Diff
+1658
View File
File diff suppressed because it is too large Load Diff
+494
View File
@@ -0,0 +1,494 @@
"""生成消融结算、信息价值、选格、押的因子的预期值 JSON(ablation.json)。
用法(在仓库根目录):
python3 tests/fixtures/rules/gen_ablation.py
只用标准库;依赖同目录的 rules_ref.py。网格积分是纯 Python,整份要跑几分钟。重跑结果逐位相同。
对应 docs/方法.md:
6.6 消融结算(撑几成、相对交互量、标签、确认/否定、分不开、五种结局)
6.7 方向收益、成分与权重、信息价值、价值减成本、选格
6.8 押的因子
4.3 ⑰ 消融中发现(采纳线以 B = f(完整) 算)
以及约定表第 17–30 条。
信息价值的两种算法:
- 全测:一维精确。本脚本不照约定 26 的"1−∏F 逐段积分",而是在有理数上用 Stieltjes 积分
E_j[min_p X_p](越小越好时 max)精确求,再换成收益(收益是 x 的同一个仿射函数)。
- 部分格:约定 27 的网格,是方法规定的确定近似,只能照规定做(浮点)。
- 另给两个只用于核对的参考:全测时也用网格算一份(gridFullVOI),只测一格时给出精确值
(exactK1VOI,相邻折点之间后验权重是常数,max(一次函数, 常数) 可以精确积分)。
生成的文件:ablation.json,格式:
{
"generator": "gen_ablation.py",
"tolerance": {...},
"models": {模型名: {params, input}}, # 几个场景共用的消融输入
"scenarios": [
{"name", "section", "params", "input", "expected", "note"}
]
}
- 模型 input:{"B": 基线分数, "full": 完整分数, "knownBest": K* 或 null, "direction",
"cells": ["p1", ...], "explanations": [{"id", "author", "cells": {"p1": {"q1","q5","q9"}, ...}}]}
params:{"eps", "lambda", "u", "r", "batchCap"}
- 场景 input 里的 "model" 字段指向 models 里的名字。
- 成分顺序 = explanations 的顺序,最后一个是开放预留(名字 "reserve")。
- 分数一律是原值;收益、VOI 是收益刻度(已乘 h、除以 G)。
"""
import json
import math
import os
import sys
import time
from fractions import Fraction as Fr
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import rules_ref as R # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
F = float
EPS, LAM, U, RR, KCAP = 0.01, 0.1, 0.5, 0.5, 2
PARAMS = {"eps": EPS, "lambda": LAM, "u": U, "r": RR, "batchCap": KCAP}
def tri(t):
return {"q1": t[0], "q5": t[1], "q9": t[2]}
# ------------------------------------------------------------------ 输入模型
MODELS = {}
# A:参考项目 calc15 的输入(只用 input 部分)。p1 = 关掉网格起点,p2 = 关掉求解流程。
C15 = [("e1", "researcher-1", (1.4, 1.75, 2.1), (1.9, 2.05, 2.18)),
("e2", "researcher-1", (1.9, 2.3, 2.55), (1.8, 1.95, 2.1)),
("e3", "researcher-1", (1.65, 2.05, 2.4), (1.8, 1.98, 2.15)),
("e4", "researcher-2", (2.3, 2.45, 2.58), (1.85, 2.02, 2.18)),
("e5", "researcher-2", (1.5, 1.95, 2.35), (1.9, 2.06, 2.2)),
("e6", "researcher-2", (1.8, 2.05, 2.3), (1.85, 2, 2.15)),
("e7", "researcher-3", (1.9, 2.15, 2.4), (1.95, 2.1, 2.2)),
("e8", "researcher-3", (2.25, 2.45, 2.55), (1.7, 1.9, 2.05)),
("e9", "researcher-3", (1.7, 1.95, 2.2), (1.85, 2, 2.15))]
MODELS["calc15"] = {
"B": 0.9597642169962064, "full": 2.6276819772628, "knownBest": 2.635, "direction": "maximize",
"cells": ["p1", "p2"],
"explanations": [{"id": i, "author": a, "cells": {"p1": tri(c1), "p2": tri(c2)}} for i, a, c1, c2 in C15]}
C15_VALID = {"p1": 2.5989997592071172, "p2": 1.7602769492805097}
# B:15-real-1 第 1 轮真实消融(三格、三人各两条解释)
R1 = {
"p1": [(1.2, 2, 2.44), (1.22, 1.4, 1.52), (1.05, 1.35, 1.95), (0.95, 1.3, 2.1), (1.15, 1.36, 1.55), (1.2, 1.42, 2.46)],
"p2": [(1.49, 1.54, 1.58), (1.49, 1.54, 1.58), (1.44, 1.53, 1.59), (1.4, 1.52, 1.62), (1.49, 1.535, 1.58), (1.5, 1.53, 1.58)],
"p3": [(2.3, 2.44, 2.45), (1.2, 1.42, 1.55), (0.85, 2.4, 2.45), (0.6, 1.45, 2.45), (1.8, 2.42, 2.58), (1.9, 2.44, 2.57)],
}
R1_AUTH = ["researcher-1", "researcher-1", "researcher-2", "researcher-2", "researcher-3", "researcher-3"]
R1_IDS = ["r1-e1-1", "r1-e1-2", "r1-e2-1", "r1-e2-2", "r1-e3-1", "r1-e3-2"]
MODELS["r1real"] = {
"B": 0.9597642169962064, "full": 1.552176048093269, "knownBest": 2.635983, "direction": "maximize",
"cells": ["p1", "p2", "p3"],
"explanations": [{"id": R1_IDS[i], "author": R1_AUTH[i], "cells": {p: tri(R1[p][i]) for p in R1}} for i in range(6)]}
R1_VALID = {"p1": 2.250011787062861, "p2": 1.9535929459502979, "p3": 2.4389662413796342}
# C:越小越好的合成例(两人、三条解释)
MODELS["minimize"] = {
"B": 0.40, "full": 0.35, "knownBest": 0.33, "direction": "minimize",
"cells": ["p1", "p2"],
"explanations": [
{"id": "e1", "author": "ra", "cells": {"p1": tri((0.36, 0.37, 0.39)), "p2": tri((0.35, 0.36, 0.37))}},
{"id": "e2", "author": "ra", "cells": {"p1": tri((0.35, 0.355, 0.36)), "p2": tri((0.36, 0.38, 0.40))}},
{"id": "e3", "author": "rb", "cells": {"p1": tri((0.37, 0.38, 0.40)), "p2": tri((0.355, 0.365, 0.38))}}]}
MIN_VALID = {"p1": 0.372, "p2": 0.361}
# D:四格合成例(calc15 的两格,再派生两格:p3 = p1 − 0.3 + 0.0013·n,p4 = p2 + 0.1 + 0.0007·n,n 是解释序号),
# p3、p4 各有 47 个不同折点、46 段 > m₄ = 44,全测网格走到"整格等宽切 m 份"(s = 0)的分支
MODELS["fourCells"] = {
"B": 0.9597642169962064, "full": 2.6276819772628, "knownBest": 2.635, "direction": "maximize",
"cells": ["p1", "p2", "p3", "p4"],
"explanations": [{"id": i, "author": a, "cells": {
"p1": tri(c1), "p2": tri(c2),
"p3": tri(tuple(round(x - 0.3 + 0.0013 * n, 6) for x in c1)),
"p4": tri(tuple(round(x + 0.1 + 0.0007 * n, 6) for x in c2))}} for n, (i, a, c1, c2) in enumerate(C15)]}
# E:收益不为正(p1 实测正好等于完整,p2 各成分都预计"关掉更好");F:同一组解释,实测落在全部成分之外
EF_EXPL = [
{"id": "e1", "author": "ra", "cells": {"p1": tri((1.9, 2.0, 2.1)), "p2": tri((2.1, 2.2, 2.3))}},
{"id": "e2", "author": "rb", "cells": {"p1": tri((1.95, 2.0, 2.05)), "p2": tri((2.05, 2.15, 2.25))}}]
MODELS["gainNotPositive"] = {"B": 1.0, "full": 2.0, "knownBest": None, "direction": "maximize",
"cells": ["p1", "p2"], "explanations": EF_EXPL}
# G:ĝ₀ < 0。多数解释预测关掉因子后分数反而比完整高,各因子事前期望收益都为负;成本按 max(0, ĝ₀) = 0 算。
MODELS["negativeG0"] = {"B": 1.0, "full": 2.0, "knownBest": None, "direction": "maximize",
"cells": ["p1", "p2"],
"explanations": [
{"id": "e1", "author": "ra", "cells": {"p1": tri((2.05, 2.15, 2.3)), "p2": tri((1.9, 2.1, 2.2))}},
{"id": "e2", "author": "rb", "cells": {"p1": tri((2.0, 2.1, 2.2)), "p2": tri((2.02, 2.08, 2.15))}},
{"id": "e3", "author": "rb", "cells": {"p1": tri((1.95, 2.05, 2.1)), "p2": tri((1.85, 2.0, 2.2))}}]}
def build(name):
m = MODELS[name]
expl = [{"id": e["id"], "author": e["author"],
"cells": {p: (c["q1"], c["q5"], c["q9"]) for p, c in e["cells"].items()}} for e in m["explanations"]]
return R.AblationModel(m["B"], m["full"], m["knownBest"], EPS, LAM, m["direction"], m["cells"], expl)
S = []
REPORT = {}
def fl(d):
return {k: F(v) for k, v in d.items()}
# ------------------------------------------------------------------ 6. 撑几成、i、标签
def settle_case(name, B, full, direction, cells, note, section="6.6;约定 17–19"):
offs = {c["factor"]: (c["value"] if c["kind"] == "valid" else None) for c in cells}
try:
G, sup = R.supports(B, full, offs, direction)
except ValueError:
S.append({"name": name, "section": section, "params": {},
"input": {"B": B, "full": full, "direction": direction, "cells": cells},
"expected": {"error": "totalGainNotPositive"}, "note": note})
return
lab, i = R.label(sup)
S.append({"name": name, "section": section, "params": {},
"input": {"B": B, "full": full, "direction": direction, "cells": cells},
"expected": {"G": F(G), "supports": {p: (None if v is None else F(v)) for p, v in sup.items()},
"interaction": None if i is None else F(i), "label": lab},
"note": note})
V = "valid"
settle_case("label_method_example_independent", 1.00, 1.50, "maximize",
[{"factor": "p1", "kind": V, "value": 1.45}, {"factor": "p2", "kind": V, "value": 1.10}],
"方法 6.6 示例:撑 0.10、0.80,i = −0.10,各管各(independent)。")
settle_case("label_calc15_substitute", 0.9597642169962064, 2.6276819772628, "maximize",
[{"factor": "p1", "kind": V, "value": C15_VALID["p1"]}, {"factor": "p2", "kind": V, "value": C15_VALID["p2"]}],
"calc15 实测:撑 0.017、0.518,i ≈ −0.465 < −0.2,替代(substitute)。")
settle_case("label_r1_negative", 0.9597642169962064, 1.552176048093269, "maximize",
[{"factor": p, "kind": V, "value": v} for p, v in R1_VALID.items()],
"真实第 1 轮:三格关掉后分数都比完整高,撑全 < 0,negative(不贴标签,i 不给)。")
settle_case("label_incomplete_with_other_outcomes", 1.00, 1.50, "maximize",
[{"factor": "p1", "kind": V, "value": 1.45}, {"factor": "p2", "kind": "insufficient"},
{"factor": "p3", "kind": "joint", "value": 1.2}, {"factor": "p4", "kind": "untested"},
{"factor": "p5", "kind": "cannotRun"}],
"只有有效分数进标签:p2–p5 都不是 valid(joint 有分数也不算),有因子没有有效格 → incomplete。")
settle_case("label_negative_beats_incomplete", 1.00, 1.50, "maximize",
[{"factor": "p1", "kind": V, "value": 1.6}, {"factor": "p2", "kind": "insufficient"}],
"表从上往下判:任一有效格撑 < 0 先命中 negative,即使另一格没有有效分数。")
settle_case("label_indispensable", 1.0, 2.0, "maximize",
[{"factor": "p1", "kind": V, "value": 1.05}, {"factor": "p2", "kind": V, "value": 1.0}],
"撑 0.95、1.0,都 > 0.9:缺一不可(indispensable),虽然 i = 0.95 > 0.2 也先命中这一条。")
settle_case("label_complementary", 1.0, 2.0, "maximize",
[{"factor": "p1", "kind": V, "value": 1.3}, {"factor": "p2", "kind": V, "value": 1.4}],
"撑 0.7、0.6,i = 0.3 > 0.2:互补(complementary)。")
settle_case("label_boundary_exact_0.9", 1.0, 2.0, "maximize",
[{"factor": "p1", "kind": V, "value": 1.0}, {"factor": "p2", "kind": V, "value": 1.125}],
"撑 1.0、0.875;0.875 不 > 0.9,不是缺一不可;i = 0.875 > 0.2 → 互补。用二进制精确数避免舍入。")
settle_case("label_minimize_substitute", 0.40, 0.35, "minimize",
[{"factor": "p1", "kind": V, "value": MIN_VALID["p1"]}, {"factor": "p2", "kind": V, "value": MIN_VALID["p2"]}],
"越小越好:G = 0.40−0.35;撑(p) = (x − 0.35)/G = 0.44、0.22;i = −0.34 → 替代。")
settle_case("label_G_not_positive", 1.50, 1.50, "maximize",
[{"factor": "p1", "kind": V, "value": 1.45}], "G = 0 时报错。")
settle_case("label_G_negative_minimize", 0.40, 0.41, "minimize",
[{"factor": "p1", "kind": V, "value": 0.405}], "越小越好时完整比基线还大,G < 0,报错。")
# ------------------------------------------------------------------ 确认/否定、分不开、消融中发现
def confirm_case(name, mname, valid, note):
m = MODELS[mname]
cells = sorted(valid, key=R.factor_key)
seqs = {}
table = {}
for e in m["explanations"]:
row = {}
for p in cells:
c = e["cells"][p]
row[p] = "confirmed" if c["q1"] <= valid[p] <= c["q9"] else "refuted"
table[e["id"]] = row
seqs.setdefault(tuple(row[p] for p in cells), []).append(e["id"])
groups = [ids for ids in seqs.values() if len(ids) >= 2]
S.append({"name": name, "section": "6.6 确认与否定、现有测法分不开;约定 21–22",
"params": {}, "input": {"model": mname, "valid": valid},
"expected": {"verdicts": table, "inseparableGroups": groups},
"note": note})
confirm_case("confirm_r1_real", "r1real", R1_VALID,
"落在 [q1,q9](含两端)记确认。分不开:在全部 valid 格上确认/否定序列相同的两条以上解释为一组(组内按解释顺序)。")
confirm_case("confirm_calc15", "calc15", C15_VALID, "calc15 两格都有效。")
confirm_case("confirm_endpoint_inclusive", "gainNotPositive", {"p1": 2.1, "p2": 2.05},
"端点:e1 的 p1 上沿正好 2.1、e2 的 p2 下沿正好 2.05,都算确认;e2 在 p1(上沿 2.05)否定,e1 在 p2(下沿 2.1)否定。")
def discovery(mname, cells, note):
m = MODELS[mname]
line = R.adoption_line(EPS, m["full"], m["knownBest"], m["direction"])
over = []
for c in cells:
if c["kind"] == "valid" or (c["kind"] == "joint" and c.get("value") is not None):
g = R.improve(c["value"], m["full"], m["direction"])
if g > line:
over.append(c)
best = None
for c in sorted(over, key=lambda c: R.factor_key(c["factor"])):
if best is None or R.improve(c["value"], best["value"], m["direction"]) > 0:
best = c
S.append({"name": "discovery_" + mname, "section": "4.3 ⑰、6.5;约定 23",
"params": {"eps": EPS}, "input": {"model": mname, "cells": cells},
"expected": {"line": F(line), "overLine": [c["factor"] for c in over],
"newBaseline": None if best is None else best["factor"]},
"note": note})
discovery("r1real", [{"factor": p, "kind": "valid", "value": v} for p, v in R1_VALID.items()],
"B 取 f(完整)=1.5522,采纳线 min(0.01,(2.635983−1.5522)/4)=0.01。三格都过线,取分数最好的 p3。")
discovery("calc15", [{"factor": "p1", "kind": "valid", "value": C15_VALID["p1"]},
{"factor": "p2", "kind": "joint", "value": 2.64}],
"采纳线 = (2.635−2.62768)/4 ≈ 0.00183;p1 低于完整不过线;p2 是构造的一起关格、分数 2.64(不是真实数据),参与并过线。")
discovery("gainNotPositive", [{"factor": "p1", "kind": "valid", "value": 2.5}, {"factor": "p2", "kind": "valid", "value": 2.5}],
"无已知最好,采纳线 ε;两格同分 2.5 都过线,取因子编号小的 p1。")
# ------------------------------------------------------------------ 7. 成分、h、先验、ĝ₀、VOI
def model_basics(name, mod):
return {
"G": F(mod.G), "h": F(mod.h),
"componentNames": mod.names,
"weights": [F(w) for w in mod.weights],
"reserve": {p: [F(lo), F(hi)] for p, (lo, hi) in mod.reserve.items()},
"componentExpectedPayoff": {n: fl(E) for n, E in zip(mod.names, mod.Epay)},
"priorExpectedPayoff": fl(mod.prior_payoffs()),
"g0": F(mod.g0()),
}
def subsets(cells):
from itertools import combinations
out = []
for n in range(1, len(cells) + 1):
out += [list(c) for c in combinations(cells, n)]
return out
VOI_CACHE = {}
def voi_of(name, mod, S_):
key = (name, tuple(S_))
if key in VOI_CACHE:
return VOI_CACHE[key]
if len(S_) == len(mod.cells):
v = (F(mod.voi_full_exact()), {"method": "exact1D"})
else:
t = time.time()
g, info = mod.voi_grid(S_)
info["method"] = "grid"
print(f" grid {name} {S_}: {time.time() - t:.1f}s", flush=True)
v = (g, info)
VOI_CACHE[key] = v
return v
def voi_scenario(name, extra_grid_full=True, partial=None):
mod = build(name)
exp = model_basics(name, mod)
full_exact = mod.voi_full_exact()
exp["voiFullExact"] = F(full_exact)
VOI_CACHE[(name, tuple(mod.cells))] = (F(full_exact), {"method": "exact1D"})
parts = []
for S_ in (partial if partial is not None else subsets(mod.cells)[:-1]):
v, info = voi_of(name, mod, S_)
row = {"cells": S_, "voiGrid": v, "grid": info}
if len(S_) == 1:
ex = F(mod.voi_partial_exact_k1(S_[0]))
row["exactK1VOI"] = ex
row["gridMinusExact"] = v - ex
parts.append(row)
exp["partial"] = parts
if extra_grid_full:
t = time.time()
g, info = mod.voi_grid(mod.cells)
print(f" grid-full {name}: {time.time() - t:.1f}s", flush=True)
exp["gridFullVOI"] = g
exp["gridFullInfo"] = info
exp["gridFullMinusExact"] = g - F(full_exact)
REPORT[name] = {"exact": F(full_exact), "grid": g, "diff": g - F(full_exact), "rel": (g - F(full_exact)) / F(full_exact)}
S.append({"name": "voi_" + name, "section": "6.7 方向收益、成分与权重、信息价值;约定 24–27",
"params": PARAMS, "input": {"model": name}, "expected": exp,
"note": "weights/componentExpectedPayoff 的成分顺序 = explanations 顺序 + reserve。"
"voiFullExact:全部格都测,一维精确(有理数 Stieltjes 积分)。partial[].voiGrid:照约定 27 的网格。"
"partial[].exactK1VOI:只测一格时的精确值,只用来看网格误差,实现不需要算。"
"gridFullVOI:全测时也用网格算一份,与 voiFullExact 对照(gridFullMinusExact)。"
"grid.cells 给出每格折点数、段数、s、小段数:网格折点在浮点上按 q1-(q5-q1)、q9+(q9-q5)、"
"L-W/2、U+W/2 计算后去重,实现应先核对这几个数一致(差一个 ulp 就会多出一段宽约 2e-16 的段,"
"例如 calc15 p1 里 1.0499999999999998 与 1.05 并存)。"})
return mod
print("VOI ...", flush=True)
mods = {}
mods["calc15"] = voi_scenario("calc15")
mods["r1real"] = voi_scenario("r1real")
mods["minimize"] = voi_scenario("minimize")
mods["fourCells"] = voi_scenario("fourCells", partial=[["p1", "p2", "p3"]])
mods["negativeG0"] = voi_scenario("negativeG0")
assert mods["negativeG0"].g0() < 0, "negativeG0 的 ĝ₀ 应为负"
# ------------------------------------------------------------------ 8. 价值、成本、净值、选格
def net_table(name, mod, bal):
g0 = mod.g0()
def net(combo):
combo = list(combo)
v, _ = voi_of(name, mod, combo)
Rh, mp = R.m_prime(bal, len(combo), KCAP, RR)
value = U * F(mp) * v
c = F(R.cost(g0, len(combo), KCAP))
return value - c
return net
def selection_scenario(name, balances, note):
mod = mods[name]
out = []
for bal in balances:
net = net_table(name, mod, bal)
sel, bestnet, table, uns = R.select_cells(mod.cells, bal, net)
rows = []
for combo, n in table:
v, _ = voi_of(name, mod, list(combo))
Rh, mp = R.m_prime(bal, len(combo), KCAP, RR)
rows.append({"combo": list(combo), "voi": v, "Rhat": Rh, "mPrime": F(mp),
"value": U * F(mp) * v, "cost": F(R.cost(mod.g0(), len(combo), KCAP)), "net": n})
# 全测规则
lim = max(0, bal - 1)
full_sel = mod.cells[:lim]
out.append({"balance": bal, "combos": rows, "selected": sel, "selectedNet": bestnet if sel else 0.0,
"unselected": uns,
"fullRule": {"selected": full_sel,
"insufficientBudget": [c for c in mod.cells if c not in full_sel]}})
S.append({"name": "select_" + name, "section": "6.7 价值减成本、选格;约定 28–29、文末",
"params": PARAMS, "input": {"model": name, "balances": balances},
"expected": {"g0": F(mod.g0()), "byBalance": out}, "note": note})
print("selection ...", flush=True)
selection_scenario("calc15", [0, 1, 2, 3, 10],
"余额 0、1 不做消融,各格 reason=insufficientBudget、bestCombo=null;余额 2 只能选单格;"
"余额 3 起可以两格全测(VOI 用一维精确值),部分格用网格。"
"R̂ = ⌈(余额−|S|)/每批上限⌉,m′ = Σ_{j<R̂} 0.5^j,成本 = 2·max(0,ĝ₀)·[(K+|S|)/(K+|S|+1) − K/(K+1)]。"
"unselected 里附“含这一格的可选组合中净值最大的那个”。fullRule = 全测规则(格数超过余额−1 时按编号取前余额−1 格)。")
selection_scenario("r1real", [0, 1, 2, 4, 12],
"真实第 1 轮三格。余额 4 时可选到三格全测;12 时 R̂ 更大、价值更高。")
selection_scenario("minimize", [2, 3, 6], "越小越好的合成例。")
selection_scenario("negativeG0", [3, 5],
"ĝ₀ < 0(各因子事前期望收益都为负):g0 照记原始负值,成本 = 2·max(0, ĝ₀)·[…] = 0,净值 = 价值。")
# 价值、成本的纯算例:R̂、m′、成本(含 余额−|S| = 0 与 ĝ₀ < 0)
vc = []
for bal, size, K, g0, voi in [(5, 1, 2, 0.01, 0.004), (13, 2, 2, 0.01, 0.004), (3, 3, 2, 0.01, 0.004),
(4, 1, 3, 0.02, 0.01), (6, 2, 2, -0.005, 0.003)]:
Rh, mp = R.m_prime(bal, size, K, RR)
value = Fr(U) * mp * R.fr(voi)
c = R.cost(g0, size, K)
vc.append({"input": {"balance": bal, "size": size, "batchCap": K, "g0": g0, "voi": voi},
"expected": {"Rhat": Rh, "mPrime": F(mp), "value": F(value), "cost": F(c), "net": F(value - c)}})
S.append({"name": "value_cost_pure", "section": "6.7 价值减成本;约定 28",
"params": {"u": U, "r": RR}, "input": {"cases": [x["input"] for x in vc]},
"expected": {"cases": [x["expected"] for x in vc]},
"note": "R̂ = ⌈(余额−|S|)/每批上限⌉,余额−|S| = 0 时 R̂ = 0、m′ = 0;m′ = Σ_{j<R̂} r^j;"
"成本 = 2·max(0,ĝ₀)·[(K+|S|)/(K+|S|+1) − K/(K+1)],ĝ₀ < 0 时成本 0。例:余额 13、|S| 2、K 2 → R̂ 6、m′ 1.96875。"})
# 纯选格规则:给定净值表(构造),测 1e-12 容差与平局
def pure_select(name, cells, bal, nets, note):
def net(combo):
return nets[",".join(combo)]
sel, bestnet, table, uns = R.select_cells(cells, bal, net)
S.append({"name": name, "section": "6.7 选格第 2–3 条;约定 29",
"params": {"tolerance": 1e-12},
"input": {"cells": cells, "balance": bal, "nets": nets},
"expected": {"selected": sel, "selectedNet": bestnet if sel else 0.0, "unselected": uns},
"note": note})
pure_select("select_pure_net_at_tolerance", ["p1", "p2"], 3,
{"p1": 1e-12, "p2": 0.9e-12, "p1,p2": 1e-12},
"净值都不大于 1e-12:一格都不测;各格 notWorthCost,附各自含它的最大净值组合(同一条规则:差不到 1e-12 视为相同,"
"先取格数少的)。p2 的附带组合曾是 {p1,p2}(1e-12,按“严格更大才替换”);改后 {p2}(0.9e-12)与它差不到 1e-12,"
"格数少,取 {p2}。")
pure_select("select_pure_just_above_tolerance", ["p1", "p2"], 3,
{"p1": 2.5e-12, "p2": 1e-12, "p1,p2": 3.0e-12},
"p1 = 2.5e-12 > 1e-12,入选;{p1,p2} = 3.0e-12 只比当前最好多 0.5e-12(不超过 1e-12),视为相同,保留格数少的 p1。")
pure_select("select_pure_tie_chain", ["p1", "p2", "p3"], 4,
{"p1": 0.01, "p2": 0.01 + 0.8e-12, "p3": 0.01 + 1.6e-12,
"p1,p2": 0.0, "p1,p3": 0.0, "p2,p3": 0.0, "p1,p2,p3": -1.0},
"最大净值 M = p3 = 0.01+1.6e-12;净值 ≥ M − 1e-12 的 p2、p3 视为相同,同格数按编号取小的 p2。"
"(预期值曾按“逐个扫描、比当前最好多 1e-12 才替换”的接力写法选 p3;审阅指出方法 6.7 是先求最大净值、"
"再在差不到 1e-12 的组合里按格数和编号挑,改后选 p2。)")
pure_select("select_pure_filter_before_max", ["p1", "p2"], 3,
{"p1": 0.6e-12, "p2": -1.0, "p1,p2": 1.5e-12},
"第二轮审阅的反例:先只留净值 > 1e-12 的组合(只剩 {p1,p2}),再在其中求最大值和容差窗口,选 {p1,p2}。"
"若先在全部组合里求 M = 1.5e-12、窗口 ≥ 0.5e-12,{p1}(0.6e-12)会因格数少被选中,而它的净值并不 > 1e-12。"
"p1、p2 都被选中,没有没测的格。")
pure_select("select_pure_tolerance_relay", ["p1", "p2"], 3,
{"p1": 1.0, "p2": 1.0 + 0.9e-12, "p1,p2": 1.0 + 1.8e-12},
"审阅给的接力反例:M = {p1,p2} = 1+1.8e-12,窗口 ≥ 1+0.8e-12 内有 {p2} 和 {p1,p2},先取格数少的 {p2}。"
"接力写法会因 {p2} 比 {p1} 只多 0.9e-12 不替换、{p1,p2} 比 {p1} 多 1.8e-12 替换而选 {p1,p2}。"
"p1 没选中,含它的组合 {p1}、{p1,p2} 里同样按这条挑:M = 1+1.8e-12,窗口内只有 {p1,p2}。")
pure_select("select_pure_budget_reason", ["p1", "p2", "p3"], 2,
{"p1": 0.002, "p2": 0.003, "p3": -0.001},
"余额 2 只能测 1 格:选 p2;p1、p3 再加进来就超过余额−1,reason = insufficientBudget。")
# ------------------------------------------------------------------ 9. 押的因子
def bet_case(name, mname, outcomes, note):
mod = mods.get(mname) or build(mname)
res = R.bet(mod, outcomes)
S.append({"name": name, "section": "6.8;约定 30",
"params": PARAMS, "input": {"model": mname, "outcomes": outcomes},
"expected": res, "note": note})
bet_case("bet_calc15_all_valid", "calc15",
{"p1": {"kind": "valid", "value": C15_VALID["p1"]}, "p2": {"kind": "valid", "value": C15_VALID["p2"]}},
"两格都有效:收益用实测换算,押 p2。weights 是按两格密度更新后的成分权重(只用于没有有效分数的格,这里只是记录)。")
bet_case("bet_calc15_p2_insufficient", "calc15",
{"p1": {"kind": "valid", "value": C15_VALID["p1"]}, "p2": {"kind": "insufficient"}},
"p2 证据不足:权重 ∝ 起始权重 × 各成分在 p1 实测处的密度,再用新权重估 p2 的期望收益。")
bet_case("bet_calc15_joint_ignored", "calc15",
{"p1": {"kind": "valid", "value": C15_VALID["p1"]}, "p2": {"kind": "joint", "value": 1.76}},
"p2 是一起关,它的分数不参与权重更新,也不当实测用;结果应与 p2 证据不足那一例完全相同。")
bet_case("bet_r1_negative_support", "r1real",
{p: {"kind": "valid", "value": v} for p, v in R1_VALID.items()},
"三格撑都 < 0:不押,negativeFactors 记“关掉反而更好”的分数(x − 完整,按方向,正数)。挑出的因子与预测收益照记。"
"另外:p2 实测 1.9536 落在 p2 所有成分(含预留 [1.06,1.94])之外,各成分在三格密度连乘全为 0,"
"权重无从更新、保持起始权重(weightsUpdated=false)——真实数据里就出现了这种情况。")
bet_case("bet_gain_not_positive", "gainNotPositive",
{"p1": {"kind": "valid", "value": 2.0}, "p2": {"kind": "untested"}},
"p1 实测正好等于完整,收益 0(撑 0,不是负);p2 各成分都预计关掉更好,估计收益为负。最大收益 0 ≤ 0,不押。")
bet_case("bet_weights_not_updatable", "gainNotPositive",
{"p1": {"kind": "valid", "value": -5.0}, "p2": {"kind": "untested"}},
"p1 实测 −5 落在所有成分(含预留)之外,密度全为 0,权重无从更新:保持起始权重,weightsUpdated=false。")
bet_case("bet_minimize", "minimize",
{"p1": {"kind": "valid", "value": MIN_VALID["p1"]}, "p2": {"kind": "cannotRun"}},
"越小越好:收益 = h·(x − 完整)/G,h = max(完整 − K*, ε/100) = 0.02。")
doc = {"generator": "gen_ablation.py",
"tolerance": {
"exact": "有理数精确算出的量(G、撑、i、h、权重、成分期望收益、ĝ₀、voiFullExact、exactK1VOI、成本、m′):绝对误差 1e-12",
"grid": "网格 VOI 及由它得到的价值、净值:|差| ≤ 1e-12 + 1e-9·|值|(逐项浮点累加顺序不同,约 1e-13 量级)",
"discrete": "标签、选中组合、原因、押的因子、状态:必须完全相同",
"gridVsExact": "同一输入全测时 gridFullVOI 与 voiFullExact 的对照(两种算法之间,不是实现与本脚本之间):"
"k ≤ 3 时 |差| ≤ 5e-4·|精确值|,k = 4 时 ≤ 3e-3·|精确值|;本脚本实测相对差 7.3e-6(calc15,k=2)、"
"4.7e-5(minimize,k=2)、1.05e-4(r1real,k=3,s=5–7)、1.0e-3(fourCells,k=4,s=1 和 0)。"
"理由:网格用小段中点代表整段,误差随小段宽度平方缩小、随格数增多(m 变小)变大;"
"s ≥ 1 时各成分在小段内是均匀分布,max 是凸函数,按 Jensen 不等式网格值不会超过精确值,"
"所以还可以断言 gridFullVOI ≤ voiFullExact + 1e-12(fourCells 有 s = 0 的格,不适用)。"},
"models": {k: {"params": PARAMS, "input": v} for k, v in MODELS.items()},
"scenarios": S}
with open(os.path.join(HERE, "ablation.json"), "w", encoding="utf-8") as f:
json.dump(doc, f, ensure_ascii=False, indent=1)
f.write("\n")
print(json.dumps(REPORT, indent=1))
+653
View File
@@ -0,0 +1,653 @@
"""生成正向、信息账、入口/余额/种子三份预期值 JSON。
用法(在仓库根目录):
python3 tests/fixtures/rules/gen_forward_info.py
只用标准库;依赖同目录的 rules_ref.py。几秒内跑完。重跑结果逐位相同。
对应 docs/方法.md:
6.1 方向、6.2 三个数→折线、6.3 开放预留与合并、6.4 入池/选批/分数尺读数、6.5 采纳线
6.9 比特/惊讶/分歧量/落点、6.10 第 4 条战绩、4.2 入口分配、第七部分余额、
以及约定表第 1–16、31–40 条。
生成的文件(都在本目录):
forward.json —— 折线、换算、合并、合并分位数、入池、V(S)、选批(批内最好期望、均匀随机)、分数尺读数、采纳线、采纳判断
info.json —— 世界比特、惊讶、分歧量、落点四段、覆盖率、战绩
misc.json —— 入口分配、余额、种子与随机抽样
JSON 格式(三份相同):
{
"generator": "gen_forward_info.py",
"tolerance": {"abs": 1e-12, "note": "..."},
"scenarios": [
{"name": 场景名, "section": "方法.md 小节 / 约定 N",
"params": {eps, lambda, direction, ...},
"input": {...}, "expected": {...}, "note": "说明"}
]
}
- 分数一律是原值;"Y" 前缀的字段是改进量(越大越好)刻度。
- 三个数写成 {"q1","q5","q9"},严格递增;点列写成 [[x, y], ...]。
- 正向候选的各人范围写成 [{"researcher", "range"}]。
- cdf = 低于 x 的把握 F(x)(折线连续,低于与不高于相同)。
- 预期数值由有理数精确算出后转成浮点;与实现比较时用绝对误差 tolerance.abs,
布尔、编号、标签、状态必须完全相同。
"""
import hashlib
import json
import os
import sys
from fractions import Fraction as Fr
from itertools import combinations
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import rules_ref as R # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
F = float
# 种子与随机(约定 39–40):选批的均匀随机场景和 misc.json 都用。
def canonical(obj):
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def seed_of(study, rnd, ver):
s = canonical({"studyId": study, "round": rnd, "version": ver})
return s, hashlib.sha256(s.encode("utf-8")).hexdigest()
class Rng:
def __init__(self, seed):
self.seed, self.counter = seed, 0
def uniform_int(self, n):
limit = 2 ** 48 - (2 ** 48 % n)
while True:
h = hashlib.sha256(f"{self.seed}:{self.counter}".encode("utf-8")).digest()
self.counter += 1
v = int.from_bytes(h[:6], "big")
if v < limit:
return v % n
def string_order(s):
"""JS 默认字符串序。只用来核对自然序的场景确实分得出两种排序。"""
return s.encode("utf-16-be")
def sample(seed, ids, size, key=R.id_key):
"""约定 40 的抽样。编号(候选、因子)一律按自然序排(c2 < c10、p2 < p10)。"""
a = sorted(ids, key=key)
rng = Rng(seed)
n = len(a)
size = min(size, n)
for i in range(size):
j = i + rng.uniform_int(n - i)
a[i], a[j] = a[j], a[i]
return sorted(a[:size], key=key)
MANY_C = [f"c{i}" for i in range(1, 13)]
MANY_P = [f"p{i}" for i in range(1, 13)]
def pts_json(pts):
return [[F(x), F(y)] for x, y in pts]
def tri(t):
return {"q1": t[0], "q5": t[1], "q9": t[2]}
def ranges_json(ts):
"""正向一个候选的各人范围:[{researcher, range}],研究者按位置编号 researcher-1、researcher-2……"""
return [{"researcher": f"researcher-{i + 1}", "range": tri(t)} for i, t in enumerate(ts)]
def write(name, scenarios, note, extra=None):
"""tolerance.abs 是默认的绝对误差;个别量另有容差时放在 extra 里,note 只解释,不另写数值。"""
doc = {"generator": "gen_forward_info.py",
"tolerance": {"abs": 1e-12, **(extra or {}), "note": note},
"scenarios": scenarios}
with open(os.path.join(HERE, name), "w", encoding="utf-8") as f:
json.dump(doc, f, ensure_ascii=False, indent=1)
f.write("\n")
# =========================================================== forward.json
S = []
# ---- 1. 折线 CDF
def cdf_case(name, t, xs, note, dens_xs=None):
p = R.polyline(*t)
d = R.single(p)
S.append({
"name": name, "section": "6.2;约定 1–4",
"params": {},
"input": {"range": tri(t), "x": xs, "densityAt": dens_xs or xs},
"expected": {
"points": pts_json(p),
"cdf": [F(d.left(x)) for x in xs],
"density": [F(R.pts_density(p, x)) for x in (dens_xs or xs)],
"mean": F(R.mean(d)),
},
"note": note})
cdf_case("cdf_method_example_2.44_2.52_2.62", (2.44, 2.52, 2.62),
[2.30, 2.36, 2.40, 2.44, 2.48, 2.52, 2.57, 2.62, 2.67, 2.72, 2.80],
"方法 6.2 示例:低于 2.57 的把握 0.70;尾巴 2.36–2.44、2.62–2.72。")
cdf_case("cdf_asymmetric_narrow", (1.2999, 1.30, 1.3005), [1.2997, 1.2998, 1.29995, 1.3, 1.3002, 1.3005, 1.30075, 1.301],
"方法第五部分的窄范围写法(不对称):低端尾巴 1.2998–1.2999,高端尾巴 1.3005–1.3010。折点上密度取右边那段。")
# ---- 越小越好的换算
b_min = 0.40
t_min = (0.30, 0.35, 0.45)
p_raw = R.polyline(*t_min)
p_y = R.improvement_pts(p_raw, b_min, "minimize")
p_y2 = R.polyline(*(R.fr(b_min) - R.fr(t_min[2]), R.fr(b_min) - R.fr(t_min[1]), R.fr(b_min) - R.fr(t_min[0])))
assert p_y == p_y2
ys = [-0.2, -0.05, 0.0, 0.05, 0.08, 0.10, 0.25]
dy = R.single(p_y)
S.append({
"name": "minimize_to_improvement", "section": "6.1;约定 5",
"params": {"direction": "minimize"},
"input": {"b": b_min, "range": tri(t_min), "y": ys},
"expected": {
"Yrange": {"q1": F(p_y[1][0]), "q5": F(p_y[2][0]), "q9": F(p_y[3][0])},
"Ypoints": pts_json(p_y),
"Ycdf": [F(dy.left(y)) for y in ys],
},
"note": "Y = b − 分数,一成九成对调:Y 的三个数 = (b−q9, b−q5, b−q1) = (−0.05, 0.05, 0.10);"
"低端尾巴宽 = 原 q9−q5 = 0.10,高端尾巴宽 = 原 q5−q1 = 0.05。"})
# ---- 2. 合并(正向、消融格)
EPS = 0.01
LAM = 0.1
B_R1 = 0.9597642169962064
R1_FWD = { # 15-real-1 第 1 轮真实的三人预测(只当输入)
"r1-v1-c1": [(2.0, 2.45, 2.6), (2.2, 2.5, 2.62), (2.2, 2.5, 2.63)],
"r1-v1-c2": [(1.85, 2.35, 2.6), (1.6, 2.35, 2.6), (1.8, 2.4, 2.62)],
"r1-v1-c3": [(2.3, 2.5, 2.62), (2.42, 2.52, 2.6), (2.1, 2.5, 2.6)],
}
R1_RESEARCHERS = ["researcher-1", "researcher-2", "researcher-3"]
# 越小越好的合成正向(b = 0.40,分数越低越好),两位研究者,c3 预期落在池外
B_MIN = 0.40
MIN_FWD = {
"c1": [(0.36, 0.38, 0.40), (0.35, 0.37, 0.39)],
"c2": [(0.33, 0.36, 0.41), (0.37, 0.39, 0.42)],
"c3": [(0.41, 0.43, 0.46), (0.40, 0.42, 0.45)],
"c4": [(0.37, 0.39, 0.40), (0.38, 0.395, 0.41)],
}
PROBE_Y = [-1.0, -0.01, 0.0, 0.5, 1.0, 1.4, 1.5, 1.6, 1.7, 2.0]
def forward_merge_case(name, cands, b, direction, probe, note):
dys, draws, (ylo, yhi), (rlo, rhi) = R.forward_dists(cands, b, direction, LAM)
exp = {"reserveY": [F(ylo), F(yhi)], "reserveRaw": [F(rlo), F(rhi)], "candidates": {}}
for c, d in dys.items():
yq = [R.quantile(d, p) for p in (Fr(1, 10), Fr(1, 2), Fr(9, 10))]
if direction == "maximize":
rawq = [R.fr(b) + q for q in yq]
else:
rawq = [R.fr(b) - q for q in reversed(yq)]
exp["candidates"][c] = {
"Ycdf": [F(d.left(y)) for y in probe],
"Yquantiles": {"q1": F(yq[0]), "q5": F(yq[1]), "q9": F(yq[2])},
"rawQuantiles": {"q1": F(rawq[0]), "q5": F(rawq[1]), "q9": F(rawq[2])},
"Ymean": F(R.mean(d)),
}
S.append({
"name": name, "section": "6.3、6.1;约定 6–8",
"params": {"lambda": LAM, "direction": direction, "eps": EPS},
"input": {"b": b, "candidates": {c: ranges_json(ts) for c, ts in cands.items()},
"Yprobe": probe},
"expected": exp, "note": note})
return dys
dys_r1 = forward_merge_case(
"forward_merge_r1_real", R1_FWD, B_R1, "maximize", PROBE_Y,
"真实第 1 轮三候选×三人(每人一份,等权)。开放预留取三候选九条折线(含尾巴)的最低首点和最高末点。"
"合并分位数在改进量上用广义逆 inf{y: P(Y≤y) ≥ p} 求,再加回 b。")
dys_min = forward_merge_case(
"forward_merge_minimize", MIN_FWD, B_MIN, "minimize", [-0.1, -0.02, -0.01, 0.0, 0.01, 0.02, 0.05],
"越小越好:逐人换成 Y = b − 分数再合并;原值分位数 = b − Y 的分位数,一成九成对调。")
# 消融一格的合并:真实第 1 轮消融 p1 格
R1_ABL_AUTH = ["researcher-1", "researcher-1", "researcher-2", "researcher-2", "researcher-3", "researcher-3"]
R1_ABL_IDS = ["r1-e1-1", "r1-e1-2", "r1-e2-1", "r1-e2-2", "r1-e3-1", "r1-e3-2"]
R1_ABL = {
"p1": [(1.2, 2, 2.44), (1.22, 1.4, 1.52), (1.05, 1.35, 1.95), (0.95, 1.3, 2.1), (1.15, 1.36, 1.55), (1.2, 1.42, 2.46)],
"p2": [(1.49, 1.54, 1.58), (1.49, 1.54, 1.58), (1.44, 1.53, 1.59), (1.4, 1.52, 1.62), (1.49, 1.535, 1.58), (1.5, 1.53, 1.58)],
"p3": [(2.3, 2.44, 2.45), (1.2, 1.42, 1.55), (0.85, 2.4, 2.45), (0.6, 1.45, 2.45), (1.8, 2.42, 2.58), (1.9, 2.44, 2.57)],
}
cd, (clo, chi) = R.cell_dist(R1_ABL["p1"], R1_ABL_AUTH, LAM)
probe = [0.5, 1.0, 1.3, 1.5, 2.0, 2.5, 3.0]
S.append({
"name": "ablation_cell_merge_r1_p1", "section": "6.3、6.7 成分与权重;约定 6–8、24",
"params": {"lambda": LAM},
"input": {"explanations": [{"id": i, "author": a, "range": tri(t)} for i, a, t in zip(R1_ABL_IDS, R1_ABL_AUTH, R1_ABL["p1"])],
"x": probe},
"expected": {
"reserve": [F(clo), F(chi)],
"weights": [F(w) for w in R.explanation_weights(R1_ABL_AUTH, LAM)] + [LAM],
"cdf": [F(cd.left(x)) for x in probe],
"quantiles": {k: F(R.quantile(cd, p)) for k, p in (("q1", Fr(1, 10)), ("q5", Fr(1, 2)), ("q9", Fr(9, 10)))},
},
"note": "真实第 1 轮消融 p1 格(关掉 p1)。三人各两条解释:每条 0.9/3/2 = 0.15,预留 0.1(权重最后一项)。"
"开放预留只看这一格六条折线。分位数在原值上求。"})
# 合并分位数:平段取左端(两条不重叠的窄折线各占一半,用 λ=0 去掉预留)
Q_PARTS = [(0.99, 1.0, 1.01), (1.99, 2.0, 2.01)]
dq = R.Dist([(Fr(1, 2), R.polyline(*Q_PARTS[0])), (Fr(1, 2), R.polyline(*Q_PARTS[1]))])
S.append({
"name": "quantile_flat_left_end", "section": "约定 8",
"params": {},
"input": {"mixture": [{"weight": 0.5, "range": tri(t)} for t in Q_PARTS], "p": [0.1, 0.5, 0.9]},
"expected": {"quantiles": [F(R.quantile(dq, p)) for p in (Fr(1, 10), Fr(1, 2), Fr(9, 10))]},
"note": "两条窄折线各占一半,中间 [1.02, 1.98] 把握走平在 0.5:一半取平段左端 1.02;一成落在第一条的"
"[0.99, 1.0] 段里(第一条自己的 0.2 分位),九成落在第二条的 [2.0, 2.01] 段里。"})
# ---- 3. 入池
def pool_case(name, cands, b, direction, note):
dys, _, _, _ = R.forward_dists(cands, b, direction, LAM)
exp = {}
for c, d in dys.items():
pr = R.pool_prob(d, EPS)
exp[c] = {"probBelowMinusEps": F(pr), "inPool": pr < Fr(1, 2)}
S.append({"name": name, "section": "6.4 入池;约定 9",
"params": {"eps": EPS, "lambda": LAM, "direction": direction},
"input": {"b": b, "candidates": {c: ranges_json(ts) for c, ts in cands.items()}},
"expected": exp, "note": note})
return exp
pool_case("pool_r1_real", R1_FWD, B_R1, "maximize", "真实第 1 轮:三个候选都远高于 b,都入池。")
pool_min = pool_case("pool_minimize", MIN_FWD, B_MIN, "minimize",
"越小越好:P(Y < −ε) = P(分数 > b+ε)。c3 的大部分把握在 0.41 以上,不入池。")
assert not pool_min["c3"]["inPool"]
# 刚好 0.5:一位研究者、一个候选,Y 的中位数正好在 −ε,折线与预留都关于 −ε 对称
EPS_H = 0.25
half = {"c1": [(0.5, 0.75, 1.0)]} # b = 1,Y 三个数 = (−0.5, −0.25, 0)
dys_h, _, _, _ = R.forward_dists(half, 1.0, "maximize", LAM)
ph = R.pool_prob(dys_h["c1"], EPS_H)
assert ph == Fr(1, 2)
float_ph = (1 - LAM) * 0.5 + LAM * 0.5
assert float_ph == 0.5
half2 = {"c1": [(0.5, 0.75 + 2 ** -10, 1.0)]}
dys_h2, _, _, _ = R.forward_dists(half2, 1.0, "maximize", LAM)
ph2 = R.pool_prob(dys_h2["c1"], EPS_H)
S.append({"name": "pool_exactly_half", "section": "6.4 入池;约定 9",
"params": {"eps": EPS_H, "lambda": LAM, "direction": "maximize"},
"input": {"b": 1.0, "cases": [{"candidates": {"c1": ranges_json(half["c1"])}},
{"candidates": {"c1": ranges_json(half2["c1"])}}]},
"expected": {"cases": [{"probBelowMinusEps": F(ph), "inPool": ph < Fr(1, 2)},
{"probBelowMinusEps": F(ph2), "inPool": ph2 < Fr(1, 2)}]},
"note": "第一例:Y 三个数 (−0.5,−0.25,0),预留 [−1.25,0.75],都以 −ε=−0.25 为中点,"
"P(Y<−ε) = 0.9×0.5+0.1×0.5 = 0.5(浮点下也正好 0.5),不严格小于一半,不入池。"
"第二例把 q5 往上挪 2⁻¹⁰,把握略小于 0.5,入池。ε 取 0.25 是为了全部是二进制精确数。"})
# ---- 4. V(S)
V_IN = {"X": [(0.5, (-0.001, 0.0, 0.001)), (0.5, (0.019, 0.02, 0.021))],
"Y": [(1, (0.009, 0.01, 0.011))]}
VD = {k: R.Dist([(w, R.polyline(*t)) for w, t in comps]) for k, comps in V_IN.items()}
S.append({"name": "V_method_example_X_Y", "section": "6.4 批内最好期望",
"params": {},
"input": {"Ymixtures": {k: [{"weight": w, "range": tri(t)} for w, t in comps] for k, comps in V_IN.items()}},
"expected": {"V": {"X": F(R.V([VD["X"]])), "Y": F(R.V([VD["Y"]])), "X,Y": F(R.V([VD["X"], VD["Y"]]))}},
"note": "方法示例换成窄范围(三个数严格递增):X 一半在 0 附近、一半在 0.02 附近;Y 在 0.01 附近。"
"输入是改进量上的混合(每个成分一个三个数)。V(X) ≈ 0.010175(0 附近那一半有一点低于 0、被截成 0),"
"V(Y) ≈ 0.01,V(X,Y) ≈ 0.015。"})
def batch_case(name, cands, b, direction, K, note, rand=None):
dys, _, _, _ = R.forward_dists(cands, b, direction, LAM)
pool = [c for c, d in dys.items() if R.pool_prob(d, EPS) < Fr(1, 2)]
table, best = R.select_batch(dys, pool, K)
pool_sorted = sorted(pool, key=R.id_key)
full = R.V([dys[c] for c in pool_sorted])
ruler = full - best[1] if pool else Fr(0)
S.append({"name": name, "section": "6.4 选批、分数尺读数;约定 10–14",
"params": {"eps": EPS, "lambda": LAM, "direction": direction, "batchCap": K},
"input": {"b": b, "balance": 100, "candidates": {c: ranges_json(ts) for c, ts in cands.items()}},
"expected": {
"pool": pool_sorted,
"k": min(K, 100),
"combos": [{"combo": c, "V": F(v)} for c, v in table],
"selected": best[0], "selectedV": F(best[1]),
"poolAllV": F(full),
"rulerReading": F(ruler)},
"note": note})
batch_case("batch_r1_real_k2", R1_FWD, B_R1, "maximize", 2,
"真实第 1 轮,每批上限 2:三选二,列出三个组合的 V。分数尺读数 = E[max(0, max_池 Y)] − V(S*)。")
batch_case("batch_minimize_out_of_pool", MIN_FWD, B_MIN, "minimize", 2,
"越小越好,c3 在池外:只在池内 {c1,c2,c4} 里选二;分数尺读数只按池内候选算(约定 13),池外的 c3 不进。")
# V 完全相等:两个候选输入完全相同,编号按自然序,"c2" < "c10"。
# 预期值曾按 JS 字符串序选 c10;审阅指出方法说的"编号最小"是数字意义上的大小,改为自然序后选 c2,组合列表也变成先 c2 后 c10。
tie = {"c2": [(2.40, 2.50, 2.60)], "c10": [(2.40, 2.50, 2.60)]}
batch_case("batch_tie_by_id", tie, 2.45, "maximize", 1,
"两个候选的范围完全相同,V 完全相等;按编号的自然序,c2 排在 c10 前面,选 c2。")
# 均匀随机选批(第十一部分;约定 12、13):从全部候选(不看入池)抽 min(k, 候选数) 个;S* 是抽中的组合,V(S*) 照算,
# 分数尺读数仍按候选池 V(池) − V(S*)。挑一个能抽中池外候选 c3 的种子,核对池外候选可以被抽中。
def uniform_case(name, cands, b, direction, K, must, note):
dys, _, _, _ = R.forward_dists(cands, b, direction, LAM)
pool = sorted([c for c, d in dys.items() if R.pool_prob(d, EPS) < Fr(1, 2)], key=R.id_key)
k = min(K, 100)
for rnd in range(1, 100):
study = "uniform-batch"
_, seed = seed_of(study, rnd, 1)
chosen = sample(seed, list(cands), min(k, len(cands)))
if must in chosen:
break
else:
raise AssertionError("找不到能抽中池外候选的种子")
assert must not in pool
v = R.V([dys[c] for c in chosen])
full = R.V([dys[c] for c in pool])
S.append({"name": name, "section": "第十一部分 均匀随机;约定 12–14",
"params": {"eps": EPS, "lambda": LAM, "direction": direction, "batchCap": K},
"input": {"b": b, "balance": 100, "seed": seed, "seedFrom": {"studyId": study, "round": rnd, "version": 1},
"candidates": {c: ranges_json(ts) for c, ts in cands.items()}},
"expected": {"pool": pool, "k": k, "selected": chosen, "selectedV": F(v),
"poolAllV": F(full), "rulerReading": F(full - v)},
"note": note})
uniform_case("batch_uniform_random_minimize", MIN_FWD, B_MIN, "minimize", 2, "c3",
"越小越好四候选,c3 在池外。均匀随机从全部四个候选里抽两个(编号按 JS 字符串序排好再做部分 Fisher–Yates),"
"这个种子抽中了池外的 c3。selectedV = V(S*);分数尺读数 = V(候选池 c1,c2,c4) − V(S*)。不列组合。")
# ---- 5. 采纳线与采纳
adopt = []
for B, K, d, note in [
(2.60, None, "maximize", "没有已知最好:ε"),
(2.60, 2.635983, "maximize", "正常:min(0.01, 0.035983/4=0.00899575)"),
(2.6359, 2.635983, "maximize", "下限:(K*−B)/4 < ε/100,取 ε/100 = 0.0001"),
(2.64, 2.635983, "maximize", "B 超过 K*:取 ε/100"),
(2.50, 2.635983, "maximize", "差距大:取上限 ε"),
(0.40, 0.38, "minimize", "越小越好:(B−K*)/4 = 0.005"),
(0.37, 0.38, "minimize", "越小越好、B 已比 K* 好:ε/100"),
]:
adopt.append({"B": B, "knownBest": K, "direction": d, "expected": F(R.adoption_line(EPS, B, K, d)), "note": note})
S.append({"name": "adoption_line_cases", "section": "6.5;约定 15",
"params": {"eps": EPS}, "input": {"cases": [{k: v for k, v in a.items() if k not in ("expected",)} for a in adopt]},
"expected": {"lines": [a["expected"] for a in adopt]},
"note": "逐例对应 input.cases。"})
def adopt_decide(B, K, d, results):
line = R.adoption_line(EPS, B, K, d)
best = None
for cid in sorted(results):
v = results[cid]
if v is None:
continue
if best is None or R.improve(v, results[best], d) > 0:
best = cid
if best is None:
return {"line": F(line), "best": None, "adopt": False}
gain = R.improve(results[best], B, d)
return {"line": F(line), "best": best, "gain": F(gain), "adopt": gain > line}
dec = []
for B, K, d, res, note in [
(2.60, 2.635983, "maximize", {"c1": 2.605, "c2": 2.6185, "c3": None}, "c2 涨 0.0185 > 0.00899575,采纳;c3 没有有效成绩"),
(2.60, None, "maximize", {"c1": 2.61, "c2": 2.605}, "正好高 ε:不采纳"),
(2.60, None, "maximize", {"c1": 2.6125, "c2": 2.6125}, "同分取编号小的 c1,涨 0.0125 > 0.01"),
(0.40, None, "minimize", {"c1": 0.385, "c2": 0.395}, "越小越好:c1 改进 0.015 > 0.01"),
]:
dec.append({"input": {"B": B, "knownBest": K, "direction": d, "results": res}, "expected": adopt_decide(B, K, d, res), "note": note})
S.append({"name": "adoption_decision_cases", "section": "6.5;约定 16",
"params": {"eps": EPS}, "input": {"cases": [x["input"] for x in dec]},
"expected": {"cases": [x["expected"] for x in dec]},
"note": "改进量 > 采纳线(严格)才采纳;同分取编号小的。第二例是'正好高 ε':浮点输入 2.61、2.60 的精确差 "
"= 0.009999999999999787(浮点减法 2.61-2.6 也是这个数),不大于 0.01,不采纳。"})
write("forward.json", S, "预期值由有理数精确算出;与浮点实现比较用绝对误差 1e-12。")
# =========================================================== info.json
S = []
DELTA = EPS / 100
# 正向:r1 真实三候选,原值上切格(b 起切),惊讶用 c3 的真实成绩
_, draws_r1, _, _ = R.forward_dists(R1_FWD, B_R1, "maximize", LAM)
fw = {}
for c, d in draws_r1.items():
fw[c] = {"worldBits": R.world_bits(d, B_R1, DELTA)}
v_c3 = 1.552176048093269
fw["r1-v1-c3"]["surprise"] = R.surprise(draws_r1["r1-v1-c3"], v_c3, B_R1, DELTA)
far = R.surprise(draws_r1["r1-v1-c3"], 10.0, B_R1, DELTA)
dis, hb, each = R.disagreement([R.single(R.polyline(*t)) for t in R1_FWD["r1-v1-c3"]], B_R1, DELTA)
S.append({"name": "bits_forward_r1_real", "section": "6.9;约定 31–33",
"params": {"eps": EPS, "lambda": LAM, "delta": DELTA, "direction": "maximize"},
"input": {"b": B_R1, "candidates": {c: ranges_json(ts) for c, ts in R1_FWD.items()},
"actual": {"r1-v1-c3": v_c3}, "farValue": 10.0},
"expected": {"candidates": fw,
"surpriseFarValue": far,
"disagreementC3": {"disagreement": dis, "avgBits": hb, "eachBits": each}},
"note": "原值上从 b 起切 Δ=ε/100 的小格,合并分布 = 三人折线 0.9 + 共同预留 0.1。"
"c3 的实测 1.552176 远低于各人范围,只落在预留里。farValue=10 在预留之外,事前把握为 0,"
"状态 priorZero(不是 0 或无穷大)。分歧量只用三人折线(不掺预留)。"})
# 消融格:r1 p1,b = 本次消融的基线
B_ABL = 0.9597642169962064
v_p1 = 2.250011787062861
persons = {}
for a, t in zip(R1_ABL_AUTH, R1_ABL["p1"]):
persons.setdefault(a, []).append(R.polyline(*t))
pd = [R.Dist([(Fr(1, len(ps)), p) for p in ps]) for ps in persons.values()]
dis2, hb2, each2 = R.disagreement(pd, B_ABL, DELTA)
S.append({"name": "bits_ablation_cell_r1_p1", "section": "6.9;约定 31–33",
"params": {"eps": EPS, "lambda": LAM, "delta": DELTA},
"input": {"b": B_ABL, "explanations": [{"id": i, "author": a, "range": tri(t)} for i, a, t in zip(R1_ABL_IDS, R1_ABL_AUTH, R1_ABL["p1"])],
"actual": v_p1},
"expected": {"worldBits": R.world_bits(cd, B_ABL, DELTA),
"surprise": R.surprise(cd, v_p1, B_ABL, DELTA),
"disagreement": dis2, "avgBits": hb2, "eachPersonBits": each2},
"note": "消融格的合并分布 = 6 条解释(每条 0.15)+ 这一格的预留 0.1。分歧量:每人取自己几条解释的等权平均作为"
"'这个人的折线',再比较三人平均的比特与各人比特的平均(不掺预留)。"})
# 越小越好的格子边界:原值上从 b 起切,b 以下是负的 j
d_min = R.forward_dists(MIN_FWD, B_MIN, "minimize", LAM)[1]["c1"]
edge_m1 = R.edge(B_MIN, DELTA, -1)
cases = []
for v in [0.3999, edge_m1, 0.4, 0.38, 0.38005]:
cases.append({"value": v, "result": R.surprise(d_min, v, B_MIN, DELTA)})
S.append({"name": "bits_minimize_bin_edges", "section": "6.9;约定 31–32",
"params": {"eps": EPS, "lambda": LAM, "delta": DELTA, "direction": "minimize"},
"input": {"b": B_MIN, "candidates": {c: ranges_json(ts) for c, ts in MIN_FWD.items()},
"candidate": "c1", "values": [c["value"] for c in cases]},
"expected": {"worldBits": R.world_bits(d_min, B_MIN, DELTA), "surprise": [c["result"] for c in cases],
"edgeMinus1": edge_m1},
"note": "越小越好也在分数原值上从 b 起切,第 j 格 [b+jΔ, b+(j+1)Δ),j 可为负。边界一律按浮点 b + j*Δ 算:"
f"b+(-1)Δ = {edge_m1!r},所以字面 0.3999 落在 j=-2 格,而值正好等于该边界时落在 j=-1 格。"
"合并分布是越小越好的那套(四候选共同预留,在原值上等价于原值折线的最低/最高点)。"})
# 落点四段、覆盖率
lc = [(2.44, 2.52, 2.62, v) for v in [2.40, 2.44, 2.50, 2.52, 2.60, 2.62, 2.65]]
S.append({"name": "landing_segments_endpoints", "section": "6.9;约定 34",
"params": {}, "input": {"cases": [{"range": tri(c[:3]), "value": c[3]} for c in lc]},
"expected": {"segments": [R.landing(*c) for c in lc],
"coverage": sum(1 for c in lc if c[0] <= c[3] <= c[2]) / len(lc)},
"note": "v<q1 below10;q1≤v<q5 from10to50;q5≤v≤q9 from50to90;v>q9 above90。端点:v=q1 归一成到一半,"
"v=q5 和 v=q9 归一半到九成。覆盖率 = 落在 [q1,q9](含端点)的比例 = 5/7。"})
S.append({"name": "coverage_no_samples", "section": "6.9;约定 34",
"params": {}, "input": {"cases": []},
"expected": {"coverage": {"status": "noSamples"}},
"note": "没有样本时返回明确状态,不是 0 或 NaN。"})
# 战绩
samples = [((2.44, 2.52, 2.62), 2.52), ((2.44, 2.52, 2.62), 2.62), ((2.44, 2.52, 2.62), 2.70),
((2.44, 2.52, 2.62), 2.44), ((2.44, 2.52, 2.62), 2.30), ((0.99, 1.0, 1.01), 1.0)]
def track(ss):
out = {"medianHigh": 0, "medianLow": 0, "pierceUpper": 0, "pierceLower": 0, "covered": 0}
for (q1, q5, q9), v in ss:
if q5 > v:
out["medianHigh"] += 1
if q5 < v:
out["medianLow"] += 1
if v > q9:
out["pierceUpper"] += 1
if v < q1:
out["pierceLower"] += 1
if q1 <= v <= q9:
out["covered"] += 1
return out
S.append({"name": "track_record_counts", "section": "6.10 第 4 条;约定 37",
"params": {},
"input": {"forward": [{"range": tri(t), "value": v} for t, v in samples[:3]],
"ablation": [{"range": tri(t), "value": v} for t, v in samples[3:]]},
"expected": {"forward": track(samples[:3]), "ablation": track(samples[3:])},
"note": "分数原值、不按方向。q5>v 中位数偏高;q5<v 偏低;v>q9 穿上沿;v<q1 穿下沿;q1≤v≤q9 包住。"
"v=q5 既不偏高也不偏低;v=q9 算包住、不算穿上沿;窄范围 (0.99,1,1.01) 实测 1 只算包住。正向与消融分开数。"})
# 作者自评 vs 合并
mq = {}
for c, d in dys_r1.items():
yq = [R.quantile(d, p) for p in (Fr(1, 10), Fr(1, 2), Fr(9, 10))]
mq[c] = tuple(F(R.fr(B_R1) + q) for q in yq)
av = [
{"candidate": "r1-v1-c3", "author": (2.1, 2.5, 2.6), "merged": mq["r1-v1-c3"], "value": v_c3, "ran": True},
{"candidate": "r1-v1-c1", "author": (2.2, 2.5, 2.63), "merged": mq["r1-v1-c1"], "value": None, "ran": False},
]
rows = []
for a in av:
if not a["ran"] or a["value"] is None:
continue
rows.append({"candidate": a["candidate"],
"q5Diff": F(R.fr(a["author"][1]) - R.fr(a["merged"][1])),
"authorSegment": R.landing(*a["author"], a["value"]),
"mergedSegment": R.landing(*a["merged"], a["value"])})
S.append({"name": "author_vs_merged", "section": "6.10 第 4 条;约定 37",
"params": {"direction": "maximize"},
"input": {"items": [{"candidate": a["candidate"], "author": tri(a["author"]), "merged": tri(a["merged"]),
"value": a["value"], "ran": a["ran"]} for a in av]},
"expected": {"rows": rows},
"note": "只收跑过且有有效成绩的候选(c1 没跑,不进)。q5Diff = 按方向的 作者q5 − 合并q5(>0 作者更乐观)。"
"合并范围取 forward.json 里 forward_merge_r1_real 的原值分位数。作者自评三个数是示意输入。"})
write("info.json", S,
"abs 用于事前把握、覆盖率、作者乐观差等普通数值。"
"bits 用于世界比特和分歧量:本脚本按有理数算出每格质量后用浮点求熵(fsum),浮点实现每格质量是两个浮点 F 值之差,"
"约 2 万到 5 万个小格累加后末位误差积到 1e-12 量级(第二轮审阅实测最大差 3.5e-12),留约 30 倍余量取 1e-10。"
"surprise 是惊讶的容差公式:本脚本的 p 是有理数精确差,浮点实现只能算 F(右端) − F(左端),"
"两个不超过 1 的浮点数相减已有几个 2⁻⁵³ 的绝对误差,p 很小时(如 2.5e-6)换成 −log₂p 就放大成约 1e-12 量级的差。"
"状态、格号、落点、计数必须完全相同。",
{"bits": 1e-10, "surprise": "1e-12 + 2^-50 / (p * ln 2)"})
# =========================================================== misc.json
S = []
ENT = [{"id": "e1", "author": "r1"}, {"id": "e2", "author": "r1"}, {"id": "e3", "author": "r2"},
{"id": "e4", "author": "r2"}, {"id": "e5", "author": "r3"}, {"id": "e6", "author": "r3"}]
def entry_case(name, rs, ents, prefs, note):
res = R.allocate_entries(rs, ents, prefs)
S.append({"name": name, "section": "4.2 分配规则;约定 38",
"params": {}, "input": {"researchers": rs, "entries": ents, "preferences": prefs},
"expected": {"assignments": res}, "note": note})
for name, rs, prefs, note in [
("entries_rule1_unique", ["r1", "r2", "r3"], {"r1": ["e1", "e3", "e5"], "r2": ["e3", "e1", "e5"], "r3": ["e6", "e1", "e2"]},
"规则 1:第 1 志愿互不相同,各得第 1 志愿。"),
("entries_rule2_author_wins", ["r1", "r2", "r3"], {"r1": ["e3", "e1", "e2"], "r2": ["e3", "e4", "e1"], "r3": ["e3", "e5", "e1"]},
"规则 2:三人都选 e3,作者 r2 在其中,归 r2;r1、r3 进入第 2 轮,各得 e1、e5(第 2 志愿)。"),
("entries_rule2_smallest_index", ["r1", "r2", "r3"], {"r1": ["e5", "e2", "e3"], "r2": ["e1", "e4", "e5"], "r3": ["e1", "e4", "e2"]},
"规则 2:r2、r3 同选 e1,作者 r1 不在其中,归编号小的 r2;第 2 轮 r3 选 e4 得到。"),
("entries_identical_prefs", ["r1", "r2", "r3"], {"r1": ["e1", "e2", "e3"], "r2": ["e1", "e2", "e3"], "r3": ["e1", "e2", "e3"]},
"三人志愿完全相同:第 1 轮 e1 归作者 r1;第 2 轮 r2、r3 选 e2,作者 r1 已分到不在其中,归 r2;"
"第 3 轮 r3 选 e3 得到。"),
("entries_rule3_exhausted", ["r1", "r2", "r3"], {"r1": ["e3", "e5"], "r2": ["e3", "e5"], "r3": ["e3", "e5"]},
"志愿用完:第 1 轮 e3 归作者 r2;第 2 轮 r1、r3 选 e5,作者 r3 在其中归 r3;r1 志愿用完,"
"按规则 3 拿剩下编号最小的 e1,choice 为 null。"),
("entries_N1", ["r1"], {"r1": ["e2", "e1"]}, "N=1:直接给第一志愿。"),
]:
ents = ENT if len(rs) > 1 else ENT[:2]
entry_case(name, rs, ents, prefs, note)
# "编号最小"按自然序判(researcher-2 < researcher-10、e2 < e10),与传入数组的次序无关:输入故意乱序给。
NAT_ENT = [{"id": "e10", "author": "researcher-2"}, {"id": "e1", "author": "researcher-3"},
{"id": "e2", "author": "researcher-3"}]
entry_case("entries_natural_rule2", ["researcher-10", "researcher-3", "researcher-2"], NAT_ENT,
{"researcher-10": ["e1"], "researcher-2": ["e1"], "researcher-3": ["e10"]},
"规则 2:researcher-10 与 researcher-2 同选 e1,作者 researcher-3 不在其中,归编号小的 researcher-2"
"(按字符串序会错给 researcher-10);researcher-3 独选 e10;researcher-10 志愿用完,按规则 3 拿剩下的 e2。")
entry_case("entries_natural_rule3", ["researcher-10", "researcher-3", "researcher-2"], NAT_ENT,
{"researcher-10": ["e1"], "researcher-2": ["e1"], "researcher-3": ["e1"]},
"三人同选 e1,作者 researcher-3 在其中归他;researcher-2、researcher-10 志愿用完,按编号从小到大拿剩下"
"编号最小的入口:researcher-2 拿 e2,researcher-10 拿 e10(按字符串序会把 e10 排在 e2 前面)。")
# 余额:第七部分示例表
rows = [("基线", ["started"]), ("第 1 轮正向", ["started"] * 2), ("第 1 轮消融", ["started"] * 2),
("第 2 轮正向", ["started"] * 2), ("第 3 轮正向", ["started"] * 2), ("第 3 轮消融", []),
("第 4 轮正向", ["started"] * 2), ("第 5 轮正向", ["started"] * 2)]
runs, out = [], []
for label_, add in rows:
runs = runs + add
b = R.balance(13, runs)
out.append({"step": label_, "added": add, **b, "forwardK": min(2, b["balance"]), "ablationMax": max(0, b["balance"] - 1)})
S.append({"name": "balance_part7_table", "section": "第七部分示例;约定 35–36",
"params": {"totalRuns": 13, "batchCap": 2},
"input": {"steps": [{"step": l, "added": a} for l, a in rows]},
"expected": {"afterEachStep": out},
"note": "每一步追加的运行记录状态,累计后算余额。余额序列 12,10,8,6,4,4,2,0。"
"forwardK = min(每批上限, 余额);ablationMax = 余额−1(余额 ≤ 1 时 0)。"})
mix = ["started", "reserved", "notStarted", "started", "reserved"]
b = R.balance(6, mix)
S.append({"name": "balance_mixed_states", "section": "第七部分;约定 35–36",
"params": {"totalRuns": 6, "batchCap": 2},
"input": {"runs": mix},
"expected": {**b, "forwardK": min(2, b["balance"]), "ablationMax": max(0, b["balance"] - 1)},
"note": "started 扣次;reserved(含状态未知)占着名额;notStarted 退还。已扣 2,占用 4,余额 2。"})
for tot, rs in [(3, ["started", "started", "started"]), (3, ["started", "started"]), (3, ["started"])]:
b = R.balance(tot, rs)
S.append({"name": f"balance_{b['balance']}", "section": "第七部分;约定 36",
"params": {"totalRuns": tot, "batchCap": 2}, "input": {"runs": rs},
"expected": {**b, "forwardK": min(2, b["balance"]), "ablationMax": max(0, b["balance"] - 1)},
"note": "余额 0、1、2 时的正向批量与消融上限。"})
for study, rnd, ver in [("study-a", 1, 1), ("15-real-1", 3, 1)]:
can, seed = seed_of(study, rnd, ver)
rng = Rng(seed)
draws = [rng.uniform_int(10) for _ in range(5)]
# 十个以上的编号:候选、因子都按自然序排好再抽;和按字符串序排抽出的结果不同,能分出用错排序的实现。
# 预期值曾让候选按字符串序抽,改为自然序后 batchSampleMany 随之变化。
many_c = sample(seed, MANY_C, 4)
many_p = sample(seed, MANY_P, 4)
assert many_c != sample(seed, MANY_C, 4, string_order), "这个种子分不出候选的排序"
assert many_p != sample(seed, MANY_P, 4, string_order), "这个种子分不出因子的排序"
first6 = [hashlib.sha256(f"{seed}:{c}".encode()).digest()[:6].hex() for c in range(3)]
S.append({"name": f"seed_{study}_r{rnd}", "section": "约定 39–40",
"params": {}, "input": {"studyId": study, "round": rnd, "version": ver,
"candidates": ["c1", "c2", "c3", "c4", "c5"], "batchSize": 2,
"cells": ["p1", "p2", "p3", "p4"], "cellCount": 2,
"candidatesMany": MANY_C, "cellsMany": MANY_P, "manyCount": 4},
"expected": {"canonical": can, "seed": seed,
"first6BytesHex": first6,
"uniformInt10First5": draws,
"batchSample": sample(seed, ["c1", "c2", "c3", "c4", "c5"], 2),
"cellSample": sample(seed, ["p1", "p2", "p3", "p4"], 2),
"batchSampleMany": many_c,
"cellSampleMany": many_p},
"note": "种子 = sha256(规范 JSON {round, studyId, version})。uniformInt:sha256(`${seed}:${counter}`) 前 6 字节大端,"
"counter 从 0 起;uniformInt10First5 是同一个生成器连续 5 次。batchSample、cellSample 各自用一个从 counter=0 "
"开始的新生成器,部分 Fisher–Yates 后取前 size 个再排序。候选、因子编号都按自然序(c2 < c10、p2 < p10);"
"*Many 两项用 12 个编号核对这一点。"})
write("misc.json", S, "全是离散结果,必须完全相同。")
print("ok")
+580
View File
@@ -0,0 +1,580 @@
{
"generator": "gen_forward_info.py",
"tolerance": {
"abs": 1e-12,
"bits": 1e-10,
"surprise": "1e-12 + 2^-50 / (p * ln 2)",
"note": "abs 用于事前把握、覆盖率、作者乐观差等普通数值。bits 用于世界比特和分歧量:本脚本按有理数算出每格质量后用浮点求熵(fsum),浮点实现每格质量是两个浮点 F 值之差,约 2 万到 5 万个小格累加后末位误差积到 1e-12 量级(第二轮审阅实测最大差 3.5e-12),留约 30 倍余量取 1e-10。surprise 是惊讶的容差公式:本脚本的 p 是有理数精确差,浮点实现只能算 F(右端) − F(左端),两个不超过 1 的浮点数相减已有几个 2⁻⁵³ 的绝对误差,p 很小时(如 2.5e-6)换成 −log₂p 就放大成约 1e-12 量级的差。状态、格号、落点、计数必须完全相同。"
},
"scenarios": [
{
"name": "bits_forward_r1_real",
"section": "6.9;约定 31–33",
"params": {
"eps": 0.01,
"lambda": 0.1,
"delta": 0.0001,
"direction": "maximize"
},
"input": {
"b": 0.9597642169962064,
"candidates": {
"r1-v1-c1": [
{
"researcher": "researcher-1",
"range": {
"q1": 2.0,
"q5": 2.45,
"q9": 2.6
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 2.2,
"q5": 2.5,
"q9": 2.62
}
},
{
"researcher": "researcher-3",
"range": {
"q1": 2.2,
"q5": 2.5,
"q9": 2.63
}
}
],
"r1-v1-c2": [
{
"researcher": "researcher-1",
"range": {
"q1": 1.85,
"q5": 2.35,
"q9": 2.6
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 1.6,
"q5": 2.35,
"q9": 2.6
}
},
{
"researcher": "researcher-3",
"range": {
"q1": 1.8,
"q5": 2.4,
"q9": 2.62
}
}
],
"r1-v1-c3": [
{
"researcher": "researcher-1",
"range": {
"q1": 2.3,
"q5": 2.5,
"q9": 2.62
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 2.42,
"q5": 2.52,
"q9": 2.6
}
},
{
"researcher": "researcher-3",
"range": {
"q1": 2.1,
"q5": 2.5,
"q9": 2.6
}
}
]
},
"actual": {
"r1-v1-c3": 1.552176048093269
},
"farValue": 10.0
},
"expected": {
"candidates": {
"r1-v1-c1": {
"worldBits": 13.424994765554958
},
"r1-v1-c2": {
"worldBits": 14.07304207309006
},
"r1-v1-c3": {
"worldBits": 13.018692667257758,
"surprise": {
"status": "ok",
"bin": 5924,
"p": 2.499999999999725e-06,
"surprise": 18.609640474436972
}
}
},
"surpriseFarValue": {
"status": "priorZero",
"bin": 90402,
"p": 0.0
},
"disagreementC3": {
"disagreement": 0.22345314430460128,
"avgBits": 12.401558575360262,
"eachBits": [
12.319392171330211,
11.527004851853361,
12.68791926998341
]
}
},
"note": "原值上从 b 起切 Δ=ε/100 的小格,合并分布 = 三人折线 0.9 + 共同预留 0.1。c3 的实测 1.552176 远低于各人范围,只落在预留里。farValue=10 在预留之外,事前把握为 0,状态 priorZero(不是 0 或无穷大)。分歧量只用三人折线(不掺预留)。"
},
{
"name": "bits_ablation_cell_r1_p1",
"section": "6.9;约定 31–33",
"params": {
"eps": 0.01,
"lambda": 0.1,
"delta": 0.0001
},
"input": {
"b": 0.9597642169962064,
"explanations": [
{
"id": "r1-e1-1",
"author": "researcher-1",
"range": {
"q1": 1.2,
"q5": 2,
"q9": 2.44
}
},
{
"id": "r1-e1-2",
"author": "researcher-1",
"range": {
"q1": 1.22,
"q5": 1.4,
"q9": 1.52
}
},
{
"id": "r1-e2-1",
"author": "researcher-2",
"range": {
"q1": 1.05,
"q5": 1.35,
"q9": 1.95
}
},
{
"id": "r1-e2-2",
"author": "researcher-2",
"range": {
"q1": 0.95,
"q5": 1.3,
"q9": 2.1
}
},
{
"id": "r1-e3-1",
"author": "researcher-3",
"range": {
"q1": 1.15,
"q5": 1.36,
"q9": 1.55
}
},
{
"id": "r1-e3-2",
"author": "researcher-3",
"range": {
"q1": 1.2,
"q5": 1.42,
"q9": 2.46
}
}
],
"actual": 2.250011787062861
},
"expected": {
"worldBits": 14.423852532506046,
"surprise": {
"status": "ok",
"bin": 12902,
"p": 2.5393497631454448e-05,
"surprise": 15.265181352865099
},
"disagreement": 0.17487769063959568,
"avgBits": 13.988099142237102,
"eachPersonBits": [
13.828510269170529,
14.016547746252732,
13.594606339369257
]
},
"note": "消融格的合并分布 = 6 条解释(每条 0.15)+ 这一格的预留 0.1。分歧量:每人取自己几条解释的等权平均作为'这个人的折线',再比较三人平均的比特与各人比特的平均(不掺预留)。"
},
{
"name": "bits_minimize_bin_edges",
"section": "6.9;约定 31–32",
"params": {
"eps": 0.01,
"lambda": 0.1,
"delta": 0.0001,
"direction": "minimize"
},
"input": {
"b": 0.4,
"candidates": {
"c1": [
{
"researcher": "researcher-1",
"range": {
"q1": 0.36,
"q5": 0.38,
"q9": 0.4
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 0.35,
"q5": 0.37,
"q9": 0.39
}
}
],
"c2": [
{
"researcher": "researcher-1",
"range": {
"q1": 0.33,
"q5": 0.36,
"q9": 0.41
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 0.37,
"q5": 0.39,
"q9": 0.42
}
}
],
"c3": [
{
"researcher": "researcher-1",
"range": {
"q1": 0.41,
"q5": 0.43,
"q9": 0.46
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 0.4,
"q5": 0.42,
"q9": 0.45
}
}
],
"c4": [
{
"researcher": "researcher-1",
"range": {
"q1": 0.37,
"q5": 0.39,
"q9": 0.4
}
},
{
"researcher": "researcher-2",
"range": {
"q1": 0.38,
"q5": 0.395,
"q9": 0.41
}
}
]
},
"candidate": "c1",
"values": [
0.3999,
0.39990000000000003,
0.4,
0.38,
0.38005
]
},
"expected": {
"worldBits": 10.045467864639367,
"surprise": [
{
"status": "ok",
"bin": -2,
"p": 0.0011513157894735564,
"surprise": 9.762500686273503
},
{
"status": "ok",
"bin": -1,
"p": 0.0011513157894735564,
"surprise": 9.762500686273503
},
{
"status": "ok",
"bin": 0,
"p": 0.0004763157894736314,
"surprise": 11.03579400590999
},
{
"status": "ok",
"bin": -200,
"p": 0.0018263157894734814,
"surprise": 9.096848040414935
},
{
"status": "ok",
"bin": -200,
"p": 0.0018263157894734814,
"surprise": 9.096848040414935
}
],
"edgeMinus1": 0.39990000000000003
},
"note": "越小越好也在分数原值上从 b 起切,第 j 格 [b+jΔ, b+(j+1)Δ),j 可为负。边界一律按浮点 b + j*Δ 算:b+(-1)Δ = 0.39990000000000003,所以字面 0.3999 落在 j=-2 格,而值正好等于该边界时落在 j=-1 格。合并分布是越小越好的那套(四候选共同预留,在原值上等价于原值折线的最低/最高点)。"
},
{
"name": "landing_segments_endpoints",
"section": "6.9;约定 34",
"params": {},
"input": {
"cases": [
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.4
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.44
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.5
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.52
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.6
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.62
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.65
}
]
},
"expected": {
"segments": [
"below10",
"from10to50",
"from10to50",
"from50to90",
"from50to90",
"from50to90",
"above90"
],
"coverage": 0.7142857142857143
},
"note": "v<q1 below10;q1≤v<q5 from10to50;q5≤v≤q9 from50to90;v>q9 above90。端点:v=q1 归一成到一半,v=q5 和 v=q9 归一半到九成。覆盖率 = 落在 [q1,q9](含端点)的比例 = 5/7。"
},
{
"name": "coverage_no_samples",
"section": "6.9;约定 34",
"params": {},
"input": {
"cases": []
},
"expected": {
"coverage": {
"status": "noSamples"
}
},
"note": "没有样本时返回明确状态,不是 0 或 NaN。"
},
{
"name": "track_record_counts",
"section": "6.10 第 4 条;约定 37",
"params": {},
"input": {
"forward": [
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.52
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.62
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.7
}
],
"ablation": [
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.44
},
{
"range": {
"q1": 2.44,
"q5": 2.52,
"q9": 2.62
},
"value": 2.3
},
{
"range": {
"q1": 0.99,
"q5": 1.0,
"q9": 1.01
},
"value": 1.0
}
]
},
"expected": {
"forward": {
"medianHigh": 0,
"medianLow": 2,
"pierceUpper": 1,
"pierceLower": 0,
"covered": 2
},
"ablation": {
"medianHigh": 2,
"medianLow": 0,
"pierceUpper": 0,
"pierceLower": 1,
"covered": 2
}
},
"note": "分数原值、不按方向。q5>v 中位数偏高;q5<v 偏低;v>q9 穿上沿;v<q1 穿下沿;q1≤v≤q9 包住。v=q5 既不偏高也不偏低;v=q9 算包住、不算穿上沿;窄范围 (0.99,1,1.01) 实测 1 只算包住。正向与消融分开数。"
},
{
"name": "author_vs_merged",
"section": "6.10 第 4 条;约定 37",
"params": {
"direction": "maximize"
},
"input": {
"items": [
{
"candidate": "r1-v1-c3",
"author": {
"q1": 2.1,
"q5": 2.5,
"q9": 2.6
},
"merged": {
"q1": 2.1289473684210525,
"q5": 2.5022627737226277,
"q9": 2.6276315789473683
},
"value": 1.552176048093269,
"ran": true
},
{
"candidate": "r1-v1-c1",
"author": {
"q1": 2.2,
"q5": 2.5,
"q9": 2.63
},
"merged": {
"q1": 1.9871428571428573,
"q5": 2.4653846153846155,
"q9": 2.6470027247956405
},
"value": null,
"ran": false
}
]
},
"expected": {
"rows": [
{
"candidate": "r1-v1-c3",
"q5Diff": -0.002262773722627731,
"authorSegment": "below10",
"mergedSegment": "below10"
}
]
},
"note": "只收跑过且有有效成绩的候选(c1 没跑,不进)。q5Diff = 按方向的 作者q5 − 合并q5(>0 作者更乐观)。合并范围取 forward.json 里 forward_merge_r1_real 的原值分位数。作者自评三个数是示意输入。"
}
]
}
+943
View File
@@ -0,0 +1,943 @@
{
"generator": "gen_forward_info.py",
"tolerance": {
"abs": 1e-12,
"note": "全是离散结果,必须完全相同。"
},
"scenarios": [
{
"name": "entries_rule1_unique",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e1",
"e3",
"e5"
],
"r2": [
"e3",
"e1",
"e5"
],
"r3": [
"e6",
"e1",
"e2"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": 1
},
"r2": {
"entry": "e3",
"choice": 1
},
"r3": {
"entry": "e6",
"choice": 1
}
}
},
"note": "规则 1:第 1 志愿互不相同,各得第 1 志愿。"
},
{
"name": "entries_rule2_author_wins",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e3",
"e1",
"e2"
],
"r2": [
"e3",
"e4",
"e1"
],
"r3": [
"e3",
"e5",
"e1"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": 2
},
"r2": {
"entry": "e3",
"choice": 1
},
"r3": {
"entry": "e5",
"choice": 2
}
}
},
"note": "规则 2:三人都选 e3,作者 r2 在其中,归 r2;r1、r3 进入第 2 轮,各得 e1、e5(第 2 志愿)。"
},
{
"name": "entries_rule2_smallest_index",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e5",
"e2",
"e3"
],
"r2": [
"e1",
"e4",
"e5"
],
"r3": [
"e1",
"e4",
"e2"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e5",
"choice": 1
},
"r2": {
"entry": "e1",
"choice": 1
},
"r3": {
"entry": "e4",
"choice": 2
}
}
},
"note": "规则 2:r2、r3 同选 e1,作者 r1 不在其中,归编号小的 r2;第 2 轮 r3 选 e4 得到。"
},
{
"name": "entries_identical_prefs",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e1",
"e2",
"e3"
],
"r2": [
"e1",
"e2",
"e3"
],
"r3": [
"e1",
"e2",
"e3"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": 1
},
"r2": {
"entry": "e2",
"choice": 2
},
"r3": {
"entry": "e3",
"choice": 3
}
}
},
"note": "三人志愿完全相同:第 1 轮 e1 归作者 r1;第 2 轮 r2、r3 选 e2,作者 r1 已分到不在其中,归 r2;第 3 轮 r3 选 e3 得到。"
},
{
"name": "entries_rule3_exhausted",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e3",
"e5"
],
"r2": [
"e3",
"e5"
],
"r3": [
"e3",
"e5"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": null
},
"r2": {
"entry": "e3",
"choice": 1
},
"r3": {
"entry": "e5",
"choice": 2
}
}
},
"note": "志愿用完:第 1 轮 e3 归作者 r2;第 2 轮 r1、r3 选 e5,作者 r3 在其中归 r3;r1 志愿用完,按规则 3 拿剩下编号最小的 e1,choice 为 null。"
},
{
"name": "entries_N1",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
}
],
"preferences": {
"r1": [
"e2",
"e1"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e2",
"choice": 1
}
}
},
"note": "N=1:直接给第一志愿。"
},
{
"name": "entries_natural_rule2",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"researcher-10",
"researcher-3",
"researcher-2"
],
"entries": [
{
"id": "e10",
"author": "researcher-2"
},
{
"id": "e1",
"author": "researcher-3"
},
{
"id": "e2",
"author": "researcher-3"
}
],
"preferences": {
"researcher-10": [
"e1"
],
"researcher-2": [
"e1"
],
"researcher-3": [
"e10"
]
}
},
"expected": {
"assignments": {
"researcher-2": {
"entry": "e1",
"choice": 1
},
"researcher-3": {
"entry": "e10",
"choice": 1
},
"researcher-10": {
"entry": "e2",
"choice": null
}
}
},
"note": "规则 2:researcher-10 与 researcher-2 同选 e1,作者 researcher-3 不在其中,归编号小的 researcher-2(按字符串序会错给 researcher-10);researcher-3 独选 e10;researcher-10 志愿用完,按规则 3 拿剩下的 e2。"
},
{
"name": "entries_natural_rule3",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"researcher-10",
"researcher-3",
"researcher-2"
],
"entries": [
{
"id": "e10",
"author": "researcher-2"
},
{
"id": "e1",
"author": "researcher-3"
},
{
"id": "e2",
"author": "researcher-3"
}
],
"preferences": {
"researcher-10": [
"e1"
],
"researcher-2": [
"e1"
],
"researcher-3": [
"e1"
]
}
},
"expected": {
"assignments": {
"researcher-2": {
"entry": "e2",
"choice": null
},
"researcher-3": {
"entry": "e1",
"choice": 1
},
"researcher-10": {
"entry": "e10",
"choice": null
}
}
},
"note": "三人同选 e1,作者 researcher-3 在其中归他;researcher-2、researcher-10 志愿用完,按编号从小到大拿剩下编号最小的入口:researcher-2 拿 e2,researcher-10 拿 e10(按字符串序会把 e10 排在 e2 前面)。"
},
{
"name": "balance_part7_table",
"section": "第七部分示例;约定 35–36",
"params": {
"totalRuns": 13,
"batchCap": 2
},
"input": {
"steps": [
{
"step": "基线",
"added": [
"started"
]
},
{
"step": "第 1 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 1 轮消融",
"added": [
"started",
"started"
]
},
{
"step": "第 2 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 3 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 3 轮消融",
"added": []
},
{
"step": "第 4 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 5 轮正向",
"added": [
"started",
"started"
]
}
]
},
"expected": {
"afterEachStep": [
{
"step": "基线",
"added": [
"started"
],
"charged": 1,
"occupied": 1,
"balance": 12,
"forwardK": 2,
"ablationMax": 11
},
{
"step": "第 1 轮正向",
"added": [
"started",
"started"
],
"charged": 3,
"occupied": 3,
"balance": 10,
"forwardK": 2,
"ablationMax": 9
},
{
"step": "第 1 轮消融",
"added": [
"started",
"started"
],
"charged": 5,
"occupied": 5,
"balance": 8,
"forwardK": 2,
"ablationMax": 7
},
{
"step": "第 2 轮正向",
"added": [
"started",
"started"
],
"charged": 7,
"occupied": 7,
"balance": 6,
"forwardK": 2,
"ablationMax": 5
},
{
"step": "第 3 轮正向",
"added": [
"started",
"started"
],
"charged": 9,
"occupied": 9,
"balance": 4,
"forwardK": 2,
"ablationMax": 3
},
{
"step": "第 3 轮消融",
"added": [],
"charged": 9,
"occupied": 9,
"balance": 4,
"forwardK": 2,
"ablationMax": 3
},
{
"step": "第 4 轮正向",
"added": [
"started",
"started"
],
"charged": 11,
"occupied": 11,
"balance": 2,
"forwardK": 2,
"ablationMax": 1
},
{
"step": "第 5 轮正向",
"added": [
"started",
"started"
],
"charged": 13,
"occupied": 13,
"balance": 0,
"forwardK": 0,
"ablationMax": 0
}
]
},
"note": "每一步追加的运行记录状态,累计后算余额。余额序列 12,10,8,6,4,4,2,0。forwardK = min(每批上限, 余额);ablationMax = 余额−1(余额 ≤ 1 时 0)。"
},
{
"name": "balance_mixed_states",
"section": "第七部分;约定 35–36",
"params": {
"totalRuns": 6,
"batchCap": 2
},
"input": {
"runs": [
"started",
"reserved",
"notStarted",
"started",
"reserved"
]
},
"expected": {
"charged": 2,
"occupied": 4,
"balance": 2,
"forwardK": 2,
"ablationMax": 1
},
"note": "started 扣次;reserved(含状态未知)占着名额;notStarted 退还。已扣 2,占用 4,余额 2。"
},
{
"name": "balance_0",
"section": "第七部分;约定 36",
"params": {
"totalRuns": 3,
"batchCap": 2
},
"input": {
"runs": [
"started",
"started",
"started"
]
},
"expected": {
"charged": 3,
"occupied": 3,
"balance": 0,
"forwardK": 0,
"ablationMax": 0
},
"note": "余额 0、1、2 时的正向批量与消融上限。"
},
{
"name": "balance_1",
"section": "第七部分;约定 36",
"params": {
"totalRuns": 3,
"batchCap": 2
},
"input": {
"runs": [
"started",
"started"
]
},
"expected": {
"charged": 2,
"occupied": 2,
"balance": 1,
"forwardK": 1,
"ablationMax": 0
},
"note": "余额 0、1、2 时的正向批量与消融上限。"
},
{
"name": "balance_2",
"section": "第七部分;约定 36",
"params": {
"totalRuns": 3,
"batchCap": 2
},
"input": {
"runs": [
"started"
]
},
"expected": {
"charged": 1,
"occupied": 1,
"balance": 2,
"forwardK": 2,
"ablationMax": 1
},
"note": "余额 0、1、2 时的正向批量与消融上限。"
},
{
"name": "seed_study-a_r1",
"section": "约定 39–40",
"params": {},
"input": {
"studyId": "study-a",
"round": 1,
"version": 1,
"candidates": [
"c1",
"c2",
"c3",
"c4",
"c5"
],
"batchSize": 2,
"cells": [
"p1",
"p2",
"p3",
"p4"
],
"cellCount": 2,
"candidatesMany": [
"c1",
"c2",
"c3",
"c4",
"c5",
"c6",
"c7",
"c8",
"c9",
"c10",
"c11",
"c12"
],
"cellsMany": [
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"p7",
"p8",
"p9",
"p10",
"p11",
"p12"
],
"manyCount": 4
},
"expected": {
"canonical": "{\"round\":1,\"studyId\":\"study-a\",\"version\":1}",
"seed": "39855ea9f581fcad6cb7710ac02151da468f2300ef72fefbbdabb3e191f62ff0",
"first6BytesHex": [
"d2d2a16d557a",
"c85c9f2d75ac",
"61392fbb6bbb"
],
"uniformInt10First5": [
2,
0,
7,
0,
2
],
"batchSample": [
"c2",
"c3"
],
"cellSample": [
"p3",
"p4"
],
"batchSampleMany": [
"c5",
"c7",
"c9",
"c10"
],
"cellSampleMany": [
"p5",
"p7",
"p9",
"p10"
]
},
"note": "种子 = sha256(规范 JSON {round, studyId, version})。uniformInt:sha256(`${seed}:${counter}`) 前 6 字节大端,counter 从 0 起;uniformInt10First5 是同一个生成器连续 5 次。batchSample、cellSample 各自用一个从 counter=0 开始的新生成器,部分 Fisher–Yates 后取前 size 个再排序。候选、因子编号都按自然序(c2 < c10、p2 < p10);*Many 两项用 12 个编号核对这一点。"
},
{
"name": "seed_15-real-1_r3",
"section": "约定 39–40",
"params": {},
"input": {
"studyId": "15-real-1",
"round": 3,
"version": 1,
"candidates": [
"c1",
"c2",
"c3",
"c4",
"c5"
],
"batchSize": 2,
"cells": [
"p1",
"p2",
"p3",
"p4"
],
"cellCount": 2,
"candidatesMany": [
"c1",
"c2",
"c3",
"c4",
"c5",
"c6",
"c7",
"c8",
"c9",
"c10",
"c11",
"c12"
],
"cellsMany": [
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"p7",
"p8",
"p9",
"p10",
"p11",
"p12"
],
"manyCount": 4
},
"expected": {
"canonical": "{\"round\":3,\"studyId\":\"15-real-1\",\"version\":1}",
"seed": "cb9cc4dbd939af39de4852c42806f3471e90bd0e16427ced09087345586506ff",
"first6BytesHex": [
"f5b64e27d768",
"0b0216f8610e",
"b48a7659e2b5"
],
"uniformInt10First5": [
8,
4,
7,
7,
6
],
"batchSample": [
"c1",
"c4"
],
"cellSample": [
"p1",
"p3"
],
"batchSampleMany": [
"c6",
"c9",
"c10",
"c11"
],
"cellSampleMany": [
"p6",
"p9",
"p10",
"p11"
]
},
"note": "种子 = sha256(规范 JSON {round, studyId, version})。uniformInt:sha256(`${seed}:${counter}`) 前 6 字节大端,counter 从 0 起;uniformInt10First5 是同一个生成器连续 5 次。batchSample、cellSample 各自用一个从 counter=0 开始的新生成器,部分 Fisher–Yates 后取前 size 个再排序。候选、因子编号都按自然序(c2 < c10、p2 < p10);*Many 两项用 12 个编号核对这一点。"
}
]
}
+784
View File
@@ -0,0 +1,784 @@
"""rules 预期值的独立参考实现(共享库,本身不生成文件)。
用途:被同目录的 gen_forward_info.py、gen_ablation.py 导入,算出预期值写成 JSON,
供 TypeScript 的 rules 测试读取。只用 Python 标准库。
独立性:本文件只照 docs/方法.md 第二部分、4.2、第六、七部分的文字,
以及用户确认的约定表(scratchpad/conventions.md,下称"约定 N")写成,
没有读过 src/rules/ 和参考项目的实现代码。
算法上刻意和"照约定逐段积分 1−∏F"的写法分开:
- 分布全部用有理数(fractions.Fraction,浮点输入先精确转成有理数)表示;
- 期望一律用 Stieltjes 积分 E[g(M)] = ∫ g(t)·F_M'(t)dt 精确求,
其中 M 是若干独立变量的最大值,F_M = ∏F(不是 ∫[1−∏F]dt 那条公式);
- 最小值通过取负号转成最大值;
- 部分格信息价值的网格算法是方法规定的确定近似(6.7、约定 27),只能照规定做,用浮点。
记号:一条"点列"是 [(x, y), ...],x 严格递增、y 从 0 到 1 不减。三个数严格递增(方法.md 第五部分),
折线连续,"低于 x"与"不高于 x"的把握相同。
"""
from fractions import Fraction as Fr
from itertools import combinations
import bisect
import functools
import math
import re
# ---------------------------------------------------------------- 基础
def fr(x):
"""浮点或整数精确转成有理数。"""
return x if isinstance(x, Fr) else Fr(x)
def to_float(x):
return float(x)
def polyline(q1, q5, q9):
"""6.2:三个数 → 五点折线(有理数)。"""
q1, q5, q9 = fr(q1), fr(q5), fr(q9)
if not (q1 < q5 < q9):
raise ValueError("三个数必须严格递增 q1 < q5 < q9(方法.md 第五部分)")
return [(q1 - (q5 - q1), Fr(0)), (q1, Fr(1, 10)), (q5, Fr(1, 2)),
(q9, Fr(9, 10)), (q9 + (q9 - q5), Fr(1))]
def uniform_pts(lo, hi):
"""[lo, hi] 上均匀分布的点列。"""
return [(fr(lo), Fr(0)), (fr(hi), Fr(1))]
def reflect(pts, b):
"""X → b − X 的点列:P(b−X < y) = 1 − P(X ≤ b−y)。点反序,横坐标 b−x,纵坐标 1−y。"""
b = fr(b)
return [(b - x, 1 - y) for (x, y) in reversed(pts)]
def shift(pts, b):
"""X → X − b。"""
b = fr(b)
return [(x - b, y) for (x, y) in pts]
def negate(pts):
"""X → −X。"""
return reflect(pts, 0)
# 单条点列"低于 x"的把握 F(x)。
def pts_left(pts, x):
xs = [p[0] for p in pts]
i = bisect.bisect_left(xs, x) - 1 # 最后一个 x_i < x
if i < 0:
return Fr(0)
if i == len(pts) - 1:
return Fr(1)
(x0, y0), (x1, y1) = pts[i], pts[i + 1]
return y0 + (y1 - y0) * (x - x0) / (x1 - x0) # x0 < x ≤ x1
def pts_density(pts, x):
"""约定 4:所在段斜率,段左闭右开(落在折点上取右边那段);范围外 0。"""
x = fr(x)
for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
if x0 <= x < x1:
return (y1 - y0) / (x1 - x0)
return Fr(0)
class Dist:
"""若干点列的加权混合(权重和为 1)。"""
def __init__(self, comps):
self.comps = [(fr(w), p) for (w, p) in comps if fr(w) != 0]
def left(self, x):
x = fr(x)
return sum((w * pts_left(p, x) for w, p in self.comps), Fr(0))
def breakpoints(self):
s = set()
for _, p in self.comps:
for x, _ in p:
s.add(x)
return s
def lo(self):
return min(self.breakpoints())
def hi(self):
return max(self.breakpoints())
def single(pts):
return Dist([(1, pts)])
# ---------------------------------------------------------------- 多项式(有理系数,低次在前)
def pmul(a, b):
out = [Fr(0)] * (len(a) + len(b) - 1)
for i, ai in enumerate(a):
if ai == 0:
continue
for j, bj in enumerate(b):
out[i + j] += ai * bj
return out
def pderiv(a):
return [i * a[i] for i in range(1, len(a))] or [Fr(0)]
def pint(a, lo, hi):
"""∫_lo^hi a(t) dt。"""
s = Fr(0)
for i, c in enumerate(a):
s += c * (hi ** (i + 1) - lo ** (i + 1)) / (i + 1)
return s
def expect_max(dists, floor0=False):
"""E[max_i X_i](各 X_i 独立),floor0=True 时求 E[max(0, max_i X_i)]。
Stieltjes 精确积分:F_M = ∏F_i 连续;相邻折点之间各 F_i 都是一次函数,∏ 是多项式,对 t·(∏)' 精确积分。
floor0 时 0 以下的把握都算在 0 上,贡献 0。
"""
bps = sorted(set().union(*[d.breakpoints() for d in dists]))
total = Fr(0)
for a, b in zip(bps, bps[1:]):
if floor0 and b <= 0:
continue
# 用平移后的变量 u = t − a 降低有理数的大小
w = b - a
poly = [Fr(1)]
for d in dists:
fa, fb = d.left(a), d.left(b)
poly = pmul(poly, [fa, (fb - fa) / w])
dp = pderiv(poly)
lo = Fr(0)
if floor0 and a < 0:
lo = -a
# t = u + a
integrand = pmul([a, Fr(1)], dp)
total += pint(integrand, lo, w)
return total
def expect_min(dists):
"""E[min_i X_i] = −E[max_i (−X_i)]。"""
neg = [Dist([(w, negate(p)) for w, p in d.comps]) for d in dists]
return -expect_max(neg)
def mean(d):
return expect_max([d])
def quantile(d, p):
"""约定 8:inf{x : F(x) ≥ p},精确求。F 连续、逐段一次;几条折线之间有空当时 F 走平,取平段左端。"""
p = fr(p)
bps = sorted(d.breakpoints())
for i, a in enumerate(bps):
if d.left(a) >= p:
return a
b = bps[i + 1]
fa, fb = d.left(a), d.left(b)
if fb >= p:
return a + (p - fa) * (b - a) / (fb - fa)
return bps[-1]
# ---------------------------------------------------------------- 方向与合并
def improvement_pts(raw_pts, b, direction):
"""约定 5:分数原值的点列 → 改进量 Y 的点列。"""
if direction == "maximize":
return shift(raw_pts, b)
if direction == "minimize":
return reflect(raw_pts, b)
raise ValueError(direction)
def improve(x, ref, direction):
"""x 相对 ref 的改进量(按方向):越大越好时 x − ref。"""
x, ref = fr(x), fr(ref)
return x - ref if direction == "maximize" else ref - x
def reserve_bounds(pts_list):
"""6.3 / 约定 6:全部点列的最低首点 L、最高末点 U,开放预留铺在 [L−W/2, U+W/2]。"""
L = min(p[0][0] for p in pts_list)
U = max(p[-1][0] for p in pts_list)
W = U - L
return L - W / 2, U + W / 2
def forward_dists(cands, b, direction, lam):
"""正向合并:cands = {候选编号: [每位研究者的三个数]},返回 (改进量上的合并分布, 原值上的合并分布, 预留端点)。
每人一份、等权;开放预留取本次选批全部候选、全部人的折线(含池外候选)。
"""
lam = fr(lam)
raw = {c: [polyline(*t) for t in triples] for c, triples in cands.items()}
allraw = [p for ps in raw.values() for p in ps]
rlo, rhi = reserve_bounds(allraw)
ylist = [improvement_pts(p, b, direction) for p in allraw]
ylo, yhi = reserve_bounds(ylist)
out_y, out_raw = {}, {}
for c, ps in raw.items():
n = len(ps)
out_y[c] = Dist([((1 - lam) / n, improvement_pts(p, b, direction)) for p in ps]
+ [(lam, uniform_pts(ylo, yhi))])
out_raw[c] = Dist([((1 - lam) / n, p) for p in ps] + [(lam, uniform_pts(rlo, rhi))])
return out_y, out_raw, (ylo, yhi), (rlo, rhi)
def explanation_weights(authors, lam):
"""6.7 / 约定 24:解释合起来 1−λ,研究者等权,同一人几条平分。返回与 authors 同序的权重。"""
lam = fr(lam)
n_auth = len(set(authors))
cnt = {a: authors.count(a) for a in set(authors)}
return [(1 - lam) / n_auth / cnt[a] for a in authors]
def cell_dist(triples, authors, lam):
"""消融一格的成分混合(原值):各解释折线 + 这一格自己的开放预留。"""
pts = [polyline(*t) for t in triples]
lo, hi = reserve_bounds(pts)
ws = explanation_weights(authors, lam)
return Dist([(w, p) for w, p in zip(ws, pts)] + [(fr(lam), uniform_pts(lo, hi))]), (lo, hi)
# ---------------------------------------------------------------- 正向:入池、V、选批、读数
def pool_prob(dy, eps):
"""约定 9:P(Y < −ε)。"""
return dy.left(-fr(eps))
def V(dys):
"""6.4:批内最好期望 E[max(0, max_c Y_c)]。空集为 0。"""
if not dys:
return Fr(0)
return expect_max(dys, floor0=True)
def _str_cmp(a, b):
"""JS 默认字符串比较(UTF-16 码元)。"""
x, y = a.encode("utf-16-be"), b.encode("utf-16-be")
return -1 if x < y else (1 if x > y else 0)
def _seg_cmp(x, y):
if not (x[0].isdigit() and y[0].isdigit()):
return _str_cmp(x, y)
tx, ty = x.lstrip("0") or "0", y.lstrip("0") or "0"
if len(tx) != len(ty):
return -1 if len(tx) < len(ty) else 1
return _str_cmp(tx, ty)
def cmp_ids(a, b):
"""编号的自然序(方法"编号最小"指数字意义上的大小):c2 < c10,researcher-2 < researcher-10,p2 < p10。
编号切成数字段和非数字段逐段比:都是数字段时先比去掉前导零后的位数、再比字符串;否则按字符串比;
段先用完的排前面;逐段都相同时按整串字符串比。候选、研究者、入口、因子、解释编号都用它。
"""
sa, sb = re.findall(r"\d+|\D+", a), re.findall(r"\d+|\D+", b)
for x, y in zip(sa, sb):
c = _seg_cmp(x, y)
if c:
return c
if len(sa) != len(sb):
return -1 if len(sa) < len(sb) else 1
return _str_cmp(a, b)
id_key = functools.cmp_to_key(cmp_ids)
def combo_key(combo):
"""组合的先后:格数少的在前,再按编号逐个比。"""
return (len(combo), [id_key(c) for c in combo])
def select_batch(dys, pool, k):
"""约定 11:枚举池中所有 min(k,|池|) 组合,V 最大;V 完全相等时按排序后编号逐个比(自然序),小者优先。"""
size = min(k, len(pool))
ids = sorted(pool, key=id_key)
table = []
for combo in combinations(ids, size):
table.append((list(combo), V([dys[c] for c in combo])))
best = None
for combo, v in table:
if best is None or v > best[1]:
best = (combo, v)
elif v == best[1] and combo_key(combo) < combo_key(best[0]):
best = (combo, v)
return table, best
def adoption_line(eps, B, Kstar, direction):
"""6.5 / 约定 15。"""
eps = fr(eps)
if Kstar is None:
return eps
room = improve(Kstar, B, direction) # 越大越好时 K* − B
line = min(eps, room / 4)
return max(line, eps / 100)
# ---------------------------------------------------------------- 消融结算
def supports(B, full, offs, direction):
"""6.6 / 约定 17:撑(p) = improvement(关掉 p, 完整)/G;G ≤ 0 抛错。offs = {因子: 分数或 None}。"""
G = improve(full, B, direction)
if G <= 0:
raise ValueError("G ≤ 0")
return G, {p: (None if x is None else improve(full, x, direction) / G) for p, x in offs.items()}
def label(sup):
"""6.6 表,从上往下。sup = {因子: 撑 或 None(没有有效分数)}。"""
vals = [v for v in sup.values() if v is not None]
if any(v < 0 for v in vals):
return "negative", None
if any(v is None for v in sup.values()):
return "incomplete", None
i = sum(vals) - 1
if all(v > Fr(9, 10) for v in vals):
return "indispensable", i
if i > Fr(1, 5):
return "complementary", i
if i < -Fr(1, 5):
return "substitute", i
return "independent", i
def factor_key(p):
"""因子编号也按自然序。"""
return id_key(p)
# ---------------------------------------------------------------- 信息价值
class AblationModel:
"""一次消融的成分模型。cells: 因子编号列表(p1…);expl: [{id, author, cells: {p: (q1,q5,q9)}}]。"""
def __init__(self, B, full, Kstar, eps, lam, direction, cells, expl):
self.B, self.full, self.eps, self.lam, self.dir = fr(B), fr(full), fr(eps), fr(lam), direction
self.Kstar = None if Kstar is None else fr(Kstar)
self.cells = sorted(cells, key=factor_key)
self.G = improve(self.full, self.B, direction)
if self.G <= 0:
raise ValueError("G ≤ 0")
self.h = Fr(1) if self.Kstar is None else max(improve(self.Kstar, self.full, direction), self.eps / 100)
authors = [e["author"] for e in expl]
self.weights = explanation_weights(authors, lam) + [self.lam]
self.names = [e["id"] for e in expl] + ["reserve"]
# comp_pts[j][p]:成分 j 在格 p 的点列
self.comp_pts = [{p: polyline(*e["cells"][p]) for p in self.cells} for e in expl]
self.triples = {p: [tuple(float(q) for q in e["cells"][p]) for e in expl] for p in self.cells}
res = {}
self.reserve = {}
for p in self.cells:
lo, hi = reserve_bounds([c[p] for c in self.comp_pts])
self.reserve[p] = (lo, hi)
res[p] = uniform_pts(lo, hi)
self.comp_pts.append(res)
# 成分在每格的期望收益(精确)
self.Epay = [{p: self.payoff(mean(single(c[p]))) for p in self.cells} for c in self.comp_pts]
def payoff(self, x):
"""约定 25:收益 = h·improvement(x, 完整)/G。"""
return self.h * improve(self.full, x, self.dir) / self.G
def prior_payoffs(self, weights=None):
w = self.weights if weights is None else weights
return {p: sum(wj * E[p] for wj, E in zip(w, self.Epay)) for p in self.cells}
def g0(self):
return max(self.prior_payoffs().values())
def voi_full_exact(self):
"""全测 VOI,一维精确:Σ_j w_j E_j[max_p 收益_p] − max_p Σ_j w_j E_j[收益_p]。
收益是 x 的同一个仿射函数,max 收益对应 min x(越大越好)或 max x(越小越好)。
"""
first = Fr(0)
for wj, c in zip(self.weights, self.comp_pts):
ds = [single(c[p]) for p in self.cells]
ext = expect_min(ds) if self.dir == "maximize" else expect_max(ds)
first += wj * self.payoff(ext)
return first - self.g0()
# ---- 网格(约定 27),浮点
@staticmethod
def m_for(k):
m = int(round(4_000_000 ** (1 / k))) # 先取浮点近似,再用整数运算校正
while m ** k > 4_000_000:
m -= 1
while (m + 1) ** k <= 4_000_000:
m += 1
return m
def cell_grid(self, p, m):
"""一格的小段:返回 [(中点, [各成分质量])] 与小段说明。"""
fpts = self.float_pts(p)
bps = sorted(set(x for pts in fpts for x, _ in pts))
nseg = len(bps) - 1
s = min(20, m // nseg)
edges = []
if s >= 1:
for a, b in zip(bps, bps[1:]):
for i in range(s):
edges.append(a + (b - a) * i / s)
edges.append(bps[-1])
else:
a, b = bps[0], bps[-1]
for i in range(m):
edges.append(a + (b - a) * i / m)
edges.append(b)
out = []
n = len(edges) - 1
for i in range(n):
lo, hi = edges[i], edges[i + 1]
mass = []
for fp in fpts:
mass.append(f_left(fp, hi) - f_left(fp, lo))
out.append(((lo + hi) / 2, mass))
return out, {"breakpoints": len(bps), "segments": nseg, "s": s, "subsegments": n}
def float_pts(self, p):
"""网格用的浮点点列:照方法原式在浮点上算尾巴 q1−(q5−q1)、q9+(q9−q5) 与预留 L−W/2、U+W/2。
网格的折点个数决定 s,横坐标差一个 ulp 就可能多出或少掉一段(例如 1.4−(1.75−1.4) 得到
1.0499999999999998,与另一条折线的 1.05 并存),所以这里必须和实现在同一套浮点式子上取折点。
"""
out = []
for (q1, q5, q9) in self.triples[p]:
out.append([(q1 - (q5 - q1), 0.0), (q1, 0.1), (q5, 0.5), (q9, 0.9), (q9 + (q9 - q5), 1.0)])
L = min(o[0][0] for o in out)
U = max(o[-1][0] for o in out)
W = U - L
out.append([(L - W / 2, 0.0), (U + W / 2, 1.0)])
return out
def voi_grid(self, S):
"""约定 27 的网格 VOI(浮点)。S:测到的格(因子编号列表)。"""
S = sorted(S, key=factor_key)
k = len(S)
m = self.m_for(k)
grids, info = [], {}
for p in S:
g, inf = self.cell_grid(p, m)
grids.append(g)
info[p] = inf
J = len(self.weights)
w = [float(x) for x in self.weights]
unmeas = [p for p in self.cells if p not in S]
Eun = [[float(self.Epay[j][q]) for j in range(J)] for q in unmeas]
pay = [[float(self.payoff(fr(mid))) for mid, _ in g] for g in grids]
total = 0.0
# 逐层展开组合:partial = (各成分联合质量向量, 已测格中点收益的最大值)
def rec(level, joint, bestpay):
nonlocal total
g = grids[level]
pl = pay[level]
if level == k - 1:
for idx, (_, mass) in enumerate(g):
jv = [joint[j] * mass[j] for j in range(J)]
marg = sum(jv)
if marg == 0:
continue
best = max(bestpay, pl[idx])
for E in Eun:
v = sum(jv[j] * E[j] for j in range(J)) / marg
if v > best:
best = v
total += best * marg
else:
for idx, (_, mass) in enumerate(g):
jv = [joint[j] * mass[j] for j in range(J)]
if not any(jv):
continue
rec(level + 1, jv, max(bestpay, pl[idx]))
rec(0, w, -math.inf)
prior = max(float(v) for v in self.prior_payoffs().values())
return total - prior, {"k": k, "m": m, "cells": info}
def voi_partial_exact_k1(self, p):
"""只测一格时的精确参考值(不是方法规定的算法,只用来估网格误差)。
相邻折点之间各成分密度是常数,后验权重不随 x 变,没测格的估计收益是常数 c;
测到的格收益是 x 的一次函数 A(x)。∫ max(A(x), c)·marg(x) dx 精确求。
"""
J = len(self.weights)
others = [q for q in self.cells if q != p]
bps = sorted(set(x for c in self.comp_pts for x, _ in c[p]))
total = Fr(0)
def best_const(wv):
s = sum(wv)
return max(sum(wv[j] * self.Epay[j][q] for j in range(J)) / s for q in others) if others else None
for a, b in zip(bps, bps[1:]):
wv = [self.weights[j] * pts_density(self.comp_pts[j][p], a) for j in range(J)]
dens = sum(wv)
if dens == 0:
continue
c = best_const(wv)
# A(x) = alpha + beta x
pa, pb = self.payoff(a), self.payoff(b)
beta = (pb - pa) / (b - a)
alpha = pa - beta * a
def integ(lo, hi, poly):
return dens * pint(poly, lo, hi)
if c is None:
total += integ(a, b, [alpha, beta])
continue
if beta == 0:
total += dens * (b - a) * max(alpha, c)
continue
xc = (c - alpha) / beta # A(xc) = c
cuts = [a] + ([xc] if a < xc < b else []) + [b]
for lo, hi in zip(cuts, cuts[1:]):
midA = alpha + beta * (lo + hi) / 2
if midA >= c:
total += integ(lo, hi, [alpha, beta])
else:
total += dens * (hi - lo) * c
return total - self.g0()
def f_left(fp, x):
xs = [q[0] for q in fp]
i = bisect.bisect_left(xs, x) - 1
if i < 0:
return 0.0
if i == len(fp) - 1:
return 1.0
(x0, y0), (x1, y1) = fp[i], fp[i + 1]
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
# ---------------------------------------------------------------- 价值、成本、选格
def m_prime(balance, size, K, r):
num = balance - size
if num <= 0:
return 0, Fr(0)
R = -(-num // K)
r = fr(r)
return R, sum((r ** j for j in range(R)), Fr(0))
def cost(g0, size, K):
g = max(Fr(0), fr(g0))
return 2 * g * (Fr(K + size, K + size + 1) - Fr(K, K + 1))
TOL = 1e-12
def top_combo(rows):
"""6.7 选格第 2 条:最大净值 M;净值 ≥ M − 1e-12 的视为相同,先取格数少的,再按因子编号逐个比取小的。"""
M = max(n for _, n in rows)
near = [r for r in rows if r[1] >= M - TOL]
return min(near, key=lambda r: combo_key(list(r[0])))
def select_cells(cells, balance, net_of):
"""约定 29 / 方法 6.7 选格:信息价值减成本选格。net_of(组合元组) → 净值(浮点)。
先只留净值 > 1e-12 的组合,一个都没有就不测;在留下的组合里按 top_combo 取。
“含这一格的净值最大组合”只是记录,不受 > 1e-12 的限制,在含这一格的全部组合里按 top_combo 取。
返回 (选中组合, 选中净值, 各组合净值表, 没选中格的说明)。
"""
cells = sorted(cells, key=factor_key)
maxn = min(len(cells), balance - 1)
table = []
for n in range(1, maxn + 1):
for combo in combinations(cells, n):
table.append((combo, net_of(combo)))
best, bestnet = (), 0.0
eligible = [r for r in table if r[1] > TOL]
if eligible:
best, bestnet = top_combo(eligible)
unselected = {}
for c in cells:
if c in best:
continue
reason = "insufficientBudget" if len(best) + 1 > balance - 1 else "notWorthCost"
cand = [(combo, net) for combo, net in table if c in combo]
if not cand:
unselected[c] = {"reason": reason, "bestCombo": None, "bestNet": None}
else:
bc, bn = top_combo(cand)
unselected[c] = {"reason": reason, "bestCombo": list(bc), "bestNet": bn}
return list(best), bestnet, table, unselected
# ---------------------------------------------------------------- 押的因子
def bet(model, outcomes):
"""6.8 / 约定 30。outcomes = {因子: {"kind": ..., "value": 分数或缺}}。"""
valid = {p: fr(o["value"]) for p, o in outcomes.items() if o["kind"] == "valid"}
J = len(model.weights)
raw = []
for j in range(J):
v = model.weights[j]
for p, x in valid.items():
v *= pts_density(model.comp_pts[j][p], x)
raw.append(v)
s = sum(raw)
updated = s != 0
w = [v / s for v in raw] if updated else list(model.weights)
est = model.prior_payoffs(w)
pay = {}
for p in model.cells:
pay[p] = model.payoff(valid[p]) if p in valid else est[p]
pick = None
for p in model.cells:
if pick is None or pay[p] > pay[pick]:
pick = p
neg = []
for p in model.cells:
if p in valid and improve(model.full, valid[p], model.dir) < 0:
neg.append({"factor": p, "offBetterBy": float(improve(model.full, valid[p], model.dir) * -1)})
if neg:
status = "noBetNegativeSupport"
elif pay[pick] <= 0:
status = "noBetNonPositiveGain"
else:
status = "bet"
return {
"weightsUpdated": updated,
"weights": {n: float(x) for n, x in zip(model.names, w)},
"payoffs": {p: float(v) for p, v in pay.items()},
"picked": pick,
"predictedGain": float(pay[pick]),
"status": status,
"negativeFactors": neg,
}
# ---------------------------------------------------------------- 信息账
def edge(b, delta, j):
"""约定 31/32:小格边界一律按浮点 b + j·Δ 算(和实现用同一套边界)。"""
return b + j * delta
def bin_of(v, b, delta):
j = math.floor((v - b) / delta)
while edge(b, delta, j) > v:
j -= 1
while edge(b, delta, j + 1) <= v:
j += 1
return j
def bin_masses(d, b, delta):
"""覆盖分布全部折点的小格质量(有理数),返回 [(j, p)]。"""
lo, hi = float(d.lo()), float(d.hi())
j0 = bin_of(lo, b, delta)
j1 = bin_of(hi, b, delta)
# 浮点的 lo/hi 与有理数端点可能差一点;两侧各多切一格,质量为 0 的格不影响熵
out = []
prev = d.left(edge(b, delta, j0 - 1))
for j in range(j0 - 1, j1 + 2):
cur = d.left(edge(b, delta, j + 1))
out.append((j, cur - prev))
prev = cur
assert d.left(edge(b, delta, j0 - 1)) == 0 and prev == 1, "切格范围没有覆盖全部质量"
return out
def world_bits(d, b, delta):
terms = []
for _, p in bin_masses(d, b, delta):
if p > 0:
pf = float(p)
terms.append(-pf * math.log2(pf))
return math.fsum(terms)
def surprise(d, v, b, delta):
j = bin_of(v, b, delta)
p = d.left(edge(b, delta, j + 1)) - d.left(edge(b, delta, j))
if p == 0:
return {"status": "priorZero", "bin": j, "p": 0.0}
return {"status": "ok", "bin": j, "p": float(p), "surprise": -math.log2(float(p))}
def disagreement(person_dists, b, delta):
avg = Dist([(Fr(1, len(person_dists)) * w, p) for d in person_dists for w, p in d.comps])
hb = world_bits(avg, b, delta)
each = [world_bits(d, b, delta) for d in person_dists]
return hb - math.fsum(each) / len(each), hb, each
def landing(q1, q5, q9, v):
"""约定 34。"""
if v < q1:
return "below10"
if v < q5:
return "from10to50"
if v <= q9:
return "from50to90"
return "above90"
# ---------------------------------------------------------------- 入口分配、余额、种子
def allocate_entries(researchers, entries, prefs):
"""4.2:researchers 研究者编号;entries = [{id, author}];prefs = {研究者: [入口编号…]}。
研究者和入口都先按编号(自然序)排好,“编号最小”按编号判,与传入次序无关。
"""
researchers = sorted(researchers, key=id_key)
entries = sorted(entries, key=lambda e: id_key(e["id"]))
author = {e["id"]: e["author"] for e in entries}
taken = {}
result = {}
maxlen = max((len(v) for v in prefs.values()), default=0)
if len(researchers) == 1:
r = researchers[0]
return {r: {"entry": prefs[r][0], "choice": 1}}
for k in range(maxlen):
want = {}
for r in researchers:
if r in result or k >= len(prefs[r]):
continue
e = prefs[r][k]
if e in taken:
continue
want.setdefault(e, []).append(r)
for e in [x["id"] for x in entries]:
if e not in want:
continue
group = want[e]
if len(group) == 1:
win = group[0]
elif author[e] in group:
win = author[e]
else:
win = min(group, key=id_key)
result[win] = {"entry": e, "choice": k + 1}
taken[e] = win
for r in researchers:
if r in result:
continue
left = [x["id"] for x in entries if x["id"] not in taken]
e = left[0]
result[r] = {"entry": e, "choice": None}
taken[e] = r
return {r: result[r] for r in researchers}
def balance(total, runs):
started = sum(1 for s in runs if s == "started")
reserved = sum(1 for s in runs if s == "reserved")
return {"charged": started, "occupied": started + reserved, "balance": total - started - reserved}