feat: 研究规则 rules 模块(任务书 02)

rules 是纯计算,src/rules 共 1761 行:方法.md 第六部分的全部公式、第七部分的余额与扣次、4.2 的入口分配,以及种子与规则登记表。

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 01:50:21 -04:00
parent bfb96ba7d7
commit 60c21ae346
37 changed files with 14067 additions and 3 deletions
+784
View File
@@ -0,0 +1,784 @@
"""rules 预期值的独立参考实现(共享库,本身不生成文件)。
用途:被同目录的 gen_forward_info.py、gen_ablation.py 导入,算出预期值写成 JSON,
供 TypeScript 的 rules 测试读取。只用 Python 标准库。
独立性:本文件只照 docs/方法.md 第二部分、4.2、第六、七部分的文字,
以及用户确认的约定表(scratchpad/conventions.md,下称"约定 N")写成,
没有读过 src/rules/ 和参考项目的实现代码。
算法上刻意和"照约定逐段积分 1−∏F"的写法分开:
- 分布全部用有理数(fractions.Fraction,浮点输入先精确转成有理数)表示;
- 期望一律用 Stieltjes 积分 E[g(M)] = ∫ g(t)·F_M'(t)dt 精确求,
其中 M 是若干独立变量的最大值,F_M = ∏F(不是 ∫[1−∏F]dt 那条公式);
- 最小值通过取负号转成最大值;
- 部分格信息价值的网格算法是方法规定的确定近似(6.7、约定 27),只能照规定做,用浮点。
记号:一条"点列"是 [(x, y), ...],x 严格递增、y 从 0 到 1 不减。三个数严格递增(方法.md 第五部分),
折线连续,"低于 x"与"不高于 x"的把握相同。
"""
from fractions import Fraction as Fr
from itertools import combinations
import bisect
import functools
import math
import re
# ---------------------------------------------------------------- 基础
def fr(x):
"""浮点或整数精确转成有理数。"""
return x if isinstance(x, Fr) else Fr(x)
def to_float(x):
return float(x)
def polyline(q1, q5, q9):
"""6.2:三个数 → 五点折线(有理数)。"""
q1, q5, q9 = fr(q1), fr(q5), fr(q9)
if not (q1 < q5 < q9):
raise ValueError("三个数必须严格递增 q1 < q5 < q9(方法.md 第五部分)")
return [(q1 - (q5 - q1), Fr(0)), (q1, Fr(1, 10)), (q5, Fr(1, 2)),
(q9, Fr(9, 10)), (q9 + (q9 - q5), Fr(1))]
def uniform_pts(lo, hi):
"""[lo, hi] 上均匀分布的点列。"""
return [(fr(lo), Fr(0)), (fr(hi), Fr(1))]
def reflect(pts, b):
"""X → b − X 的点列:P(b−X < y) = 1 − P(X ≤ b−y)。点反序,横坐标 b−x,纵坐标 1−y。"""
b = fr(b)
return [(b - x, 1 - y) for (x, y) in reversed(pts)]
def shift(pts, b):
"""X → X − b。"""
b = fr(b)
return [(x - b, y) for (x, y) in pts]
def negate(pts):
"""X → −X。"""
return reflect(pts, 0)
# 单条点列"低于 x"的把握 F(x)。
def pts_left(pts, x):
xs = [p[0] for p in pts]
i = bisect.bisect_left(xs, x) - 1 # 最后一个 x_i < x
if i < 0:
return Fr(0)
if i == len(pts) - 1:
return Fr(1)
(x0, y0), (x1, y1) = pts[i], pts[i + 1]
return y0 + (y1 - y0) * (x - x0) / (x1 - x0) # x0 < x ≤ x1
def pts_density(pts, x):
"""约定 4:所在段斜率,段左闭右开(落在折点上取右边那段);范围外 0。"""
x = fr(x)
for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
if x0 <= x < x1:
return (y1 - y0) / (x1 - x0)
return Fr(0)
class Dist:
"""若干点列的加权混合(权重和为 1)。"""
def __init__(self, comps):
self.comps = [(fr(w), p) for (w, p) in comps if fr(w) != 0]
def left(self, x):
x = fr(x)
return sum((w * pts_left(p, x) for w, p in self.comps), Fr(0))
def breakpoints(self):
s = set()
for _, p in self.comps:
for x, _ in p:
s.add(x)
return s
def lo(self):
return min(self.breakpoints())
def hi(self):
return max(self.breakpoints())
def single(pts):
return Dist([(1, pts)])
# ---------------------------------------------------------------- 多项式(有理系数,低次在前)
def pmul(a, b):
out = [Fr(0)] * (len(a) + len(b) - 1)
for i, ai in enumerate(a):
if ai == 0:
continue
for j, bj in enumerate(b):
out[i + j] += ai * bj
return out
def pderiv(a):
return [i * a[i] for i in range(1, len(a))] or [Fr(0)]
def pint(a, lo, hi):
"""∫_lo^hi a(t) dt。"""
s = Fr(0)
for i, c in enumerate(a):
s += c * (hi ** (i + 1) - lo ** (i + 1)) / (i + 1)
return s
def expect_max(dists, floor0=False):
"""E[max_i X_i](各 X_i 独立),floor0=True 时求 E[max(0, max_i X_i)]。
Stieltjes 精确积分:F_M = ∏F_i 连续;相邻折点之间各 F_i 都是一次函数,∏ 是多项式,对 t·(∏)' 精确积分。
floor0 时 0 以下的把握都算在 0 上,贡献 0。
"""
bps = sorted(set().union(*[d.breakpoints() for d in dists]))
total = Fr(0)
for a, b in zip(bps, bps[1:]):
if floor0 and b <= 0:
continue
# 用平移后的变量 u = t − a 降低有理数的大小
w = b - a
poly = [Fr(1)]
for d in dists:
fa, fb = d.left(a), d.left(b)
poly = pmul(poly, [fa, (fb - fa) / w])
dp = pderiv(poly)
lo = Fr(0)
if floor0 and a < 0:
lo = -a
# t = u + a
integrand = pmul([a, Fr(1)], dp)
total += pint(integrand, lo, w)
return total
def expect_min(dists):
"""E[min_i X_i] = −E[max_i (−X_i)]。"""
neg = [Dist([(w, negate(p)) for w, p in d.comps]) for d in dists]
return -expect_max(neg)
def mean(d):
return expect_max([d])
def quantile(d, p):
"""约定 8:inf{x : F(x) ≥ p},精确求。F 连续、逐段一次;几条折线之间有空当时 F 走平,取平段左端。"""
p = fr(p)
bps = sorted(d.breakpoints())
for i, a in enumerate(bps):
if d.left(a) >= p:
return a
b = bps[i + 1]
fa, fb = d.left(a), d.left(b)
if fb >= p:
return a + (p - fa) * (b - a) / (fb - fa)
return bps[-1]
# ---------------------------------------------------------------- 方向与合并
def improvement_pts(raw_pts, b, direction):
"""约定 5:分数原值的点列 → 改进量 Y 的点列。"""
if direction == "maximize":
return shift(raw_pts, b)
if direction == "minimize":
return reflect(raw_pts, b)
raise ValueError(direction)
def improve(x, ref, direction):
"""x 相对 ref 的改进量(按方向):越大越好时 x − ref。"""
x, ref = fr(x), fr(ref)
return x - ref if direction == "maximize" else ref - x
def reserve_bounds(pts_list):
"""6.3 / 约定 6:全部点列的最低首点 L、最高末点 U,开放预留铺在 [L−W/2, U+W/2]。"""
L = min(p[0][0] for p in pts_list)
U = max(p[-1][0] for p in pts_list)
W = U - L
return L - W / 2, U + W / 2
def forward_dists(cands, b, direction, lam):
"""正向合并:cands = {候选编号: [每位研究者的三个数]},返回 (改进量上的合并分布, 原值上的合并分布, 预留端点)。
每人一份、等权;开放预留取本次选批全部候选、全部人的折线(含池外候选)。
"""
lam = fr(lam)
raw = {c: [polyline(*t) for t in triples] for c, triples in cands.items()}
allraw = [p for ps in raw.values() for p in ps]
rlo, rhi = reserve_bounds(allraw)
ylist = [improvement_pts(p, b, direction) for p in allraw]
ylo, yhi = reserve_bounds(ylist)
out_y, out_raw = {}, {}
for c, ps in raw.items():
n = len(ps)
out_y[c] = Dist([((1 - lam) / n, improvement_pts(p, b, direction)) for p in ps]
+ [(lam, uniform_pts(ylo, yhi))])
out_raw[c] = Dist([((1 - lam) / n, p) for p in ps] + [(lam, uniform_pts(rlo, rhi))])
return out_y, out_raw, (ylo, yhi), (rlo, rhi)
def explanation_weights(authors, lam):
"""6.7 / 约定 24:解释合起来 1−λ,研究者等权,同一人几条平分。返回与 authors 同序的权重。"""
lam = fr(lam)
n_auth = len(set(authors))
cnt = {a: authors.count(a) for a in set(authors)}
return [(1 - lam) / n_auth / cnt[a] for a in authors]
def cell_dist(triples, authors, lam):
"""消融一格的成分混合(原值):各解释折线 + 这一格自己的开放预留。"""
pts = [polyline(*t) for t in triples]
lo, hi = reserve_bounds(pts)
ws = explanation_weights(authors, lam)
return Dist([(w, p) for w, p in zip(ws, pts)] + [(fr(lam), uniform_pts(lo, hi))]), (lo, hi)
# ---------------------------------------------------------------- 正向:入池、V、选批、读数
def pool_prob(dy, eps):
"""约定 9:P(Y < −ε)。"""
return dy.left(-fr(eps))
def V(dys):
"""6.4:批内最好期望 E[max(0, max_c Y_c)]。空集为 0。"""
if not dys:
return Fr(0)
return expect_max(dys, floor0=True)
def _str_cmp(a, b):
"""JS 默认字符串比较(UTF-16 码元)。"""
x, y = a.encode("utf-16-be"), b.encode("utf-16-be")
return -1 if x < y else (1 if x > y else 0)
def _seg_cmp(x, y):
if not (x[0].isdigit() and y[0].isdigit()):
return _str_cmp(x, y)
tx, ty = x.lstrip("0") or "0", y.lstrip("0") or "0"
if len(tx) != len(ty):
return -1 if len(tx) < len(ty) else 1
return _str_cmp(tx, ty)
def cmp_ids(a, b):
"""编号的自然序(方法"编号最小"指数字意义上的大小):c2 < c10,researcher-2 < researcher-10,p2 < p10。
编号切成数字段和非数字段逐段比:都是数字段时先比去掉前导零后的位数、再比字符串;否则按字符串比;
段先用完的排前面;逐段都相同时按整串字符串比。候选、研究者、入口、因子、解释编号都用它。
"""
sa, sb = re.findall(r"\d+|\D+", a), re.findall(r"\d+|\D+", b)
for x, y in zip(sa, sb):
c = _seg_cmp(x, y)
if c:
return c
if len(sa) != len(sb):
return -1 if len(sa) < len(sb) else 1
return _str_cmp(a, b)
id_key = functools.cmp_to_key(cmp_ids)
def combo_key(combo):
"""组合的先后:格数少的在前,再按编号逐个比。"""
return (len(combo), [id_key(c) for c in combo])
def select_batch(dys, pool, k):
"""约定 11:枚举池中所有 min(k,|池|) 组合,V 最大;V 完全相等时按排序后编号逐个比(自然序),小者优先。"""
size = min(k, len(pool))
ids = sorted(pool, key=id_key)
table = []
for combo in combinations(ids, size):
table.append((list(combo), V([dys[c] for c in combo])))
best = None
for combo, v in table:
if best is None or v > best[1]:
best = (combo, v)
elif v == best[1] and combo_key(combo) < combo_key(best[0]):
best = (combo, v)
return table, best
def adoption_line(eps, B, Kstar, direction):
"""6.5 / 约定 15。"""
eps = fr(eps)
if Kstar is None:
return eps
room = improve(Kstar, B, direction) # 越大越好时 K* − B
line = min(eps, room / 4)
return max(line, eps / 100)
# ---------------------------------------------------------------- 消融结算
def supports(B, full, offs, direction):
"""6.6 / 约定 17:撑(p) = improvement(关掉 p, 完整)/G;G ≤ 0 抛错。offs = {因子: 分数或 None}。"""
G = improve(full, B, direction)
if G <= 0:
raise ValueError("G ≤ 0")
return G, {p: (None if x is None else improve(full, x, direction) / G) for p, x in offs.items()}
def label(sup):
"""6.6 表,从上往下。sup = {因子: 撑 或 None(没有有效分数)}。"""
vals = [v for v in sup.values() if v is not None]
if any(v < 0 for v in vals):
return "negative", None
if any(v is None for v in sup.values()):
return "incomplete", None
i = sum(vals) - 1
if all(v > Fr(9, 10) for v in vals):
return "indispensable", i
if i > Fr(1, 5):
return "complementary", i
if i < -Fr(1, 5):
return "substitute", i
return "independent", i
def factor_key(p):
"""因子编号也按自然序。"""
return id_key(p)
# ---------------------------------------------------------------- 信息价值
class AblationModel:
"""一次消融的成分模型。cells: 因子编号列表(p1…);expl: [{id, author, cells: {p: (q1,q5,q9)}}]。"""
def __init__(self, B, full, Kstar, eps, lam, direction, cells, expl):
self.B, self.full, self.eps, self.lam, self.dir = fr(B), fr(full), fr(eps), fr(lam), direction
self.Kstar = None if Kstar is None else fr(Kstar)
self.cells = sorted(cells, key=factor_key)
self.G = improve(self.full, self.B, direction)
if self.G <= 0:
raise ValueError("G ≤ 0")
self.h = Fr(1) if self.Kstar is None else max(improve(self.Kstar, self.full, direction), self.eps / 100)
authors = [e["author"] for e in expl]
self.weights = explanation_weights(authors, lam) + [self.lam]
self.names = [e["id"] for e in expl] + ["reserve"]
# comp_pts[j][p]:成分 j 在格 p 的点列
self.comp_pts = [{p: polyline(*e["cells"][p]) for p in self.cells} for e in expl]
self.triples = {p: [tuple(float(q) for q in e["cells"][p]) for e in expl] for p in self.cells}
res = {}
self.reserve = {}
for p in self.cells:
lo, hi = reserve_bounds([c[p] for c in self.comp_pts])
self.reserve[p] = (lo, hi)
res[p] = uniform_pts(lo, hi)
self.comp_pts.append(res)
# 成分在每格的期望收益(精确)
self.Epay = [{p: self.payoff(mean(single(c[p]))) for p in self.cells} for c in self.comp_pts]
def payoff(self, x):
"""约定 25:收益 = h·improvement(x, 完整)/G。"""
return self.h * improve(self.full, x, self.dir) / self.G
def prior_payoffs(self, weights=None):
w = self.weights if weights is None else weights
return {p: sum(wj * E[p] for wj, E in zip(w, self.Epay)) for p in self.cells}
def g0(self):
return max(self.prior_payoffs().values())
def voi_full_exact(self):
"""全测 VOI,一维精确:Σ_j w_j E_j[max_p 收益_p] − max_p Σ_j w_j E_j[收益_p]。
收益是 x 的同一个仿射函数,max 收益对应 min x(越大越好)或 max x(越小越好)。
"""
first = Fr(0)
for wj, c in zip(self.weights, self.comp_pts):
ds = [single(c[p]) for p in self.cells]
ext = expect_min(ds) if self.dir == "maximize" else expect_max(ds)
first += wj * self.payoff(ext)
return first - self.g0()
# ---- 网格(约定 27),浮点
@staticmethod
def m_for(k):
m = int(round(4_000_000 ** (1 / k))) # 先取浮点近似,再用整数运算校正
while m ** k > 4_000_000:
m -= 1
while (m + 1) ** k <= 4_000_000:
m += 1
return m
def cell_grid(self, p, m):
"""一格的小段:返回 [(中点, [各成分质量])] 与小段说明。"""
fpts = self.float_pts(p)
bps = sorted(set(x for pts in fpts for x, _ in pts))
nseg = len(bps) - 1
s = min(20, m // nseg)
edges = []
if s >= 1:
for a, b in zip(bps, bps[1:]):
for i in range(s):
edges.append(a + (b - a) * i / s)
edges.append(bps[-1])
else:
a, b = bps[0], bps[-1]
for i in range(m):
edges.append(a + (b - a) * i / m)
edges.append(b)
out = []
n = len(edges) - 1
for i in range(n):
lo, hi = edges[i], edges[i + 1]
mass = []
for fp in fpts:
mass.append(f_left(fp, hi) - f_left(fp, lo))
out.append(((lo + hi) / 2, mass))
return out, {"breakpoints": len(bps), "segments": nseg, "s": s, "subsegments": n}
def float_pts(self, p):
"""网格用的浮点点列:照方法原式在浮点上算尾巴 q1−(q5−q1)、q9+(q9−q5) 与预留 L−W/2、U+W/2。
网格的折点个数决定 s,横坐标差一个 ulp 就可能多出或少掉一段(例如 1.4−(1.75−1.4) 得到
1.0499999999999998,与另一条折线的 1.05 并存),所以这里必须和实现在同一套浮点式子上取折点。
"""
out = []
for (q1, q5, q9) in self.triples[p]:
out.append([(q1 - (q5 - q1), 0.0), (q1, 0.1), (q5, 0.5), (q9, 0.9), (q9 + (q9 - q5), 1.0)])
L = min(o[0][0] for o in out)
U = max(o[-1][0] for o in out)
W = U - L
out.append([(L - W / 2, 0.0), (U + W / 2, 1.0)])
return out
def voi_grid(self, S):
"""约定 27 的网格 VOI(浮点)。S:测到的格(因子编号列表)。"""
S = sorted(S, key=factor_key)
k = len(S)
m = self.m_for(k)
grids, info = [], {}
for p in S:
g, inf = self.cell_grid(p, m)
grids.append(g)
info[p] = inf
J = len(self.weights)
w = [float(x) for x in self.weights]
unmeas = [p for p in self.cells if p not in S]
Eun = [[float(self.Epay[j][q]) for j in range(J)] for q in unmeas]
pay = [[float(self.payoff(fr(mid))) for mid, _ in g] for g in grids]
total = 0.0
# 逐层展开组合:partial = (各成分联合质量向量, 已测格中点收益的最大值)
def rec(level, joint, bestpay):
nonlocal total
g = grids[level]
pl = pay[level]
if level == k - 1:
for idx, (_, mass) in enumerate(g):
jv = [joint[j] * mass[j] for j in range(J)]
marg = sum(jv)
if marg == 0:
continue
best = max(bestpay, pl[idx])
for E in Eun:
v = sum(jv[j] * E[j] for j in range(J)) / marg
if v > best:
best = v
total += best * marg
else:
for idx, (_, mass) in enumerate(g):
jv = [joint[j] * mass[j] for j in range(J)]
if not any(jv):
continue
rec(level + 1, jv, max(bestpay, pl[idx]))
rec(0, w, -math.inf)
prior = max(float(v) for v in self.prior_payoffs().values())
return total - prior, {"k": k, "m": m, "cells": info}
def voi_partial_exact_k1(self, p):
"""只测一格时的精确参考值(不是方法规定的算法,只用来估网格误差)。
相邻折点之间各成分密度是常数,后验权重不随 x 变,没测格的估计收益是常数 c;
测到的格收益是 x 的一次函数 A(x)。∫ max(A(x), c)·marg(x) dx 精确求。
"""
J = len(self.weights)
others = [q for q in self.cells if q != p]
bps = sorted(set(x for c in self.comp_pts for x, _ in c[p]))
total = Fr(0)
def best_const(wv):
s = sum(wv)
return max(sum(wv[j] * self.Epay[j][q] for j in range(J)) / s for q in others) if others else None
for a, b in zip(bps, bps[1:]):
wv = [self.weights[j] * pts_density(self.comp_pts[j][p], a) for j in range(J)]
dens = sum(wv)
if dens == 0:
continue
c = best_const(wv)
# A(x) = alpha + beta x
pa, pb = self.payoff(a), self.payoff(b)
beta = (pb - pa) / (b - a)
alpha = pa - beta * a
def integ(lo, hi, poly):
return dens * pint(poly, lo, hi)
if c is None:
total += integ(a, b, [alpha, beta])
continue
if beta == 0:
total += dens * (b - a) * max(alpha, c)
continue
xc = (c - alpha) / beta # A(xc) = c
cuts = [a] + ([xc] if a < xc < b else []) + [b]
for lo, hi in zip(cuts, cuts[1:]):
midA = alpha + beta * (lo + hi) / 2
if midA >= c:
total += integ(lo, hi, [alpha, beta])
else:
total += dens * (hi - lo) * c
return total - self.g0()
def f_left(fp, x):
xs = [q[0] for q in fp]
i = bisect.bisect_left(xs, x) - 1
if i < 0:
return 0.0
if i == len(fp) - 1:
return 1.0
(x0, y0), (x1, y1) = fp[i], fp[i + 1]
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
# ---------------------------------------------------------------- 价值、成本、选格
def m_prime(balance, size, K, r):
num = balance - size
if num <= 0:
return 0, Fr(0)
R = -(-num // K)
r = fr(r)
return R, sum((r ** j for j in range(R)), Fr(0))
def cost(g0, size, K):
g = max(Fr(0), fr(g0))
return 2 * g * (Fr(K + size, K + size + 1) - Fr(K, K + 1))
TOL = 1e-12
def top_combo(rows):
"""6.7 选格第 2 条:最大净值 M;净值 ≥ M − 1e-12 的视为相同,先取格数少的,再按因子编号逐个比取小的。"""
M = max(n for _, n in rows)
near = [r for r in rows if r[1] >= M - TOL]
return min(near, key=lambda r: combo_key(list(r[0])))
def select_cells(cells, balance, net_of):
"""约定 29 / 方法 6.7 选格:信息价值减成本选格。net_of(组合元组) → 净值(浮点)。
先只留净值 > 1e-12 的组合,一个都没有就不测;在留下的组合里按 top_combo 取。
“含这一格的净值最大组合”只是记录,不受 > 1e-12 的限制,在含这一格的全部组合里按 top_combo 取。
返回 (选中组合, 选中净值, 各组合净值表, 没选中格的说明)。
"""
cells = sorted(cells, key=factor_key)
maxn = min(len(cells), balance - 1)
table = []
for n in range(1, maxn + 1):
for combo in combinations(cells, n):
table.append((combo, net_of(combo)))
best, bestnet = (), 0.0
eligible = [r for r in table if r[1] > TOL]
if eligible:
best, bestnet = top_combo(eligible)
unselected = {}
for c in cells:
if c in best:
continue
reason = "insufficientBudget" if len(best) + 1 > balance - 1 else "notWorthCost"
cand = [(combo, net) for combo, net in table if c in combo]
if not cand:
unselected[c] = {"reason": reason, "bestCombo": None, "bestNet": None}
else:
bc, bn = top_combo(cand)
unselected[c] = {"reason": reason, "bestCombo": list(bc), "bestNet": bn}
return list(best), bestnet, table, unselected
# ---------------------------------------------------------------- 押的因子
def bet(model, outcomes):
"""6.8 / 约定 30。outcomes = {因子: {"kind": ..., "value": 分数或缺}}。"""
valid = {p: fr(o["value"]) for p, o in outcomes.items() if o["kind"] == "valid"}
J = len(model.weights)
raw = []
for j in range(J):
v = model.weights[j]
for p, x in valid.items():
v *= pts_density(model.comp_pts[j][p], x)
raw.append(v)
s = sum(raw)
updated = s != 0
w = [v / s for v in raw] if updated else list(model.weights)
est = model.prior_payoffs(w)
pay = {}
for p in model.cells:
pay[p] = model.payoff(valid[p]) if p in valid else est[p]
pick = None
for p in model.cells:
if pick is None or pay[p] > pay[pick]:
pick = p
neg = []
for p in model.cells:
if p in valid and improve(model.full, valid[p], model.dir) < 0:
neg.append({"factor": p, "offBetterBy": float(improve(model.full, valid[p], model.dir) * -1)})
if neg:
status = "noBetNegativeSupport"
elif pay[pick] <= 0:
status = "noBetNonPositiveGain"
else:
status = "bet"
return {
"weightsUpdated": updated,
"weights": {n: float(x) for n, x in zip(model.names, w)},
"payoffs": {p: float(v) for p, v in pay.items()},
"picked": pick,
"predictedGain": float(pay[pick]),
"status": status,
"negativeFactors": neg,
}
# ---------------------------------------------------------------- 信息账
def edge(b, delta, j):
"""约定 31/32:小格边界一律按浮点 b + j·Δ 算(和实现用同一套边界)。"""
return b + j * delta
def bin_of(v, b, delta):
j = math.floor((v - b) / delta)
while edge(b, delta, j) > v:
j -= 1
while edge(b, delta, j + 1) <= v:
j += 1
return j
def bin_masses(d, b, delta):
"""覆盖分布全部折点的小格质量(有理数),返回 [(j, p)]。"""
lo, hi = float(d.lo()), float(d.hi())
j0 = bin_of(lo, b, delta)
j1 = bin_of(hi, b, delta)
# 浮点的 lo/hi 与有理数端点可能差一点;两侧各多切一格,质量为 0 的格不影响熵
out = []
prev = d.left(edge(b, delta, j0 - 1))
for j in range(j0 - 1, j1 + 2):
cur = d.left(edge(b, delta, j + 1))
out.append((j, cur - prev))
prev = cur
assert d.left(edge(b, delta, j0 - 1)) == 0 and prev == 1, "切格范围没有覆盖全部质量"
return out
def world_bits(d, b, delta):
terms = []
for _, p in bin_masses(d, b, delta):
if p > 0:
pf = float(p)
terms.append(-pf * math.log2(pf))
return math.fsum(terms)
def surprise(d, v, b, delta):
j = bin_of(v, b, delta)
p = d.left(edge(b, delta, j + 1)) - d.left(edge(b, delta, j))
if p == 0:
return {"status": "priorZero", "bin": j, "p": 0.0}
return {"status": "ok", "bin": j, "p": float(p), "surprise": -math.log2(float(p))}
def disagreement(person_dists, b, delta):
avg = Dist([(Fr(1, len(person_dists)) * w, p) for d in person_dists for w, p in d.comps])
hb = world_bits(avg, b, delta)
each = [world_bits(d, b, delta) for d in person_dists]
return hb - math.fsum(each) / len(each), hb, each
def landing(q1, q5, q9, v):
"""约定 34。"""
if v < q1:
return "below10"
if v < q5:
return "from10to50"
if v <= q9:
return "from50to90"
return "above90"
# ---------------------------------------------------------------- 入口分配、余额、种子
def allocate_entries(researchers, entries, prefs):
"""4.2:researchers 研究者编号;entries = [{id, author}];prefs = {研究者: [入口编号…]}。
研究者和入口都先按编号(自然序)排好,“编号最小”按编号判,与传入次序无关。
"""
researchers = sorted(researchers, key=id_key)
entries = sorted(entries, key=lambda e: id_key(e["id"]))
author = {e["id"]: e["author"] for e in entries}
taken = {}
result = {}
maxlen = max((len(v) for v in prefs.values()), default=0)
if len(researchers) == 1:
r = researchers[0]
return {r: {"entry": prefs[r][0], "choice": 1}}
for k in range(maxlen):
want = {}
for r in researchers:
if r in result or k >= len(prefs[r]):
continue
e = prefs[r][k]
if e in taken:
continue
want.setdefault(e, []).append(r)
for e in [x["id"] for x in entries]:
if e not in want:
continue
group = want[e]
if len(group) == 1:
win = group[0]
elif author[e] in group:
win = author[e]
else:
win = min(group, key=id_key)
result[win] = {"entry": e, "choice": k + 1}
taken[e] = win
for r in researchers:
if r in result:
continue
left = [x["id"] for x in entries if x["id"] not in taken]
e = left[0]
result[r] = {"entry": e, "choice": None}
taken[e] = r
return {r: result[r] for r in researchers}
def balance(total, runs):
started = sum(1 for s in runs if s == "started")
reserved = sum(1 for s in runs if s == "reserved")
return {"charged": started, "occupied": started + reserved, "balance": total - started - reserved}