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:
Vendored
+282
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+3862
File diff suppressed because it is too large
Load Diff
Vendored
+1658
File diff suppressed because it is too large
Load Diff
Vendored
+494
@@ -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
@@ -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")
|
||||
Vendored
+580
@@ -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 的原值分位数。作者自评三个数是示意输入。"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+943
@@ -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 个编号核对这一点。"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+784
@@ -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}
|
||||
Reference in New Issue
Block a user