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

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

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

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

495 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""生成消融结算、信息价值、选格、押的因子的预期值 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))