feat(evolution): validate.py — pure block validation decision functions (#7)
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""core/evolution/validate.py — 块验证纯决策函数。
|
||||
|
||||
算法 #7(块顺序验证)的局部实现:pair_block 逐题比对基线与候选、
|
||||
classify_quadrants 四象限分类、compute_accuracy 纯算术准确率。
|
||||
|
||||
三个函数均为纯函数,无副作用、无外部依赖。
|
||||
"""
|
||||
|
||||
from core.evolution.types import PairResult, QuadrantClassification
|
||||
|
||||
|
||||
def pair_block(
|
||||
baseline: dict[str, bool],
|
||||
candidate: dict[str, bool],
|
||||
question_ids: list[str],
|
||||
) -> PairResult:
|
||||
"""逐题比对基线与候选对错,统计翻转。
|
||||
|
||||
参数:
|
||||
baseline: 基线臂每题正确性映射。
|
||||
candidate: 候选臂每题正确性映射。
|
||||
question_ids: 参与比对的题目 ID 列表。
|
||||
|
||||
返回:
|
||||
PairResult,包含 w(基线错→候选对翻转数)、l(基线对→候选错翻转数)
|
||||
和 observed(每题的 (基线, 候选) 对错记录)。
|
||||
"""
|
||||
w = l = 0 # noqa: E741 — 数学记号 W/L(win/loss),与 gate.py 一致
|
||||
observed: dict[str, tuple[bool, bool]] = {}
|
||||
for qid in question_ids:
|
||||
b, c = baseline[qid], candidate[qid]
|
||||
observed[qid] = (b, c)
|
||||
if not b and c:
|
||||
w += 1
|
||||
elif b and not c:
|
||||
l += 1 # noqa: E741
|
||||
return PairResult(w=w, l=l, observed=observed)
|
||||
|
||||
|
||||
def classify_quadrants(
|
||||
observed: dict[str, tuple[bool, bool]],
|
||||
) -> QuadrantClassification:
|
||||
"""按 (baseline, candidate) 四组分类,各组内 sorted。
|
||||
|
||||
参数:
|
||||
observed: 每题的 (基线是否正确, 候选是否正确) 记录。
|
||||
|
||||
返回:
|
||||
QuadrantClassification,四个象限各含排序后的题目 ID 列表。
|
||||
"""
|
||||
improvements: list[str] = []
|
||||
regressions: list[str] = []
|
||||
persistent_fails: list[str] = []
|
||||
stable_successes: list[str] = []
|
||||
for qid, (prev, curr) in observed.items():
|
||||
if not prev and curr:
|
||||
improvements.append(qid)
|
||||
elif prev and not curr:
|
||||
regressions.append(qid)
|
||||
elif not prev and not curr:
|
||||
persistent_fails.append(qid)
|
||||
else:
|
||||
stable_successes.append(qid)
|
||||
return QuadrantClassification(
|
||||
improvements=sorted(improvements),
|
||||
regressions=sorted(regressions),
|
||||
persistent_fails=sorted(persistent_fails),
|
||||
stable_successes=sorted(stable_successes),
|
||||
)
|
||||
|
||||
|
||||
def compute_accuracy(
|
||||
correctness: dict[str, bool],
|
||||
question_ids: list[str],
|
||||
) -> float:
|
||||
"""纯算术:sum(correct) / len(ids)。
|
||||
|
||||
参数:
|
||||
correctness: 每题正确性映射。
|
||||
question_ids: 参与计算的题目 ID 列表。
|
||||
|
||||
返回:
|
||||
准确率浮点数。question_ids 为空时抛出 ZeroDivisionError。
|
||||
"""
|
||||
return sum(correctness[qid] for qid in question_ids) / len(question_ids)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""tests/unit/test_validate.py — 块验证纯决策函数的单元测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from core.evolution.validate import classify_quadrants, compute_accuracy, pair_block
|
||||
|
||||
|
||||
class TestPairBlock:
|
||||
"""pair_block 逐题比对基线与候选的翻转统计测试。"""
|
||||
|
||||
def test_basic_flips(self) -> None:
|
||||
"""基本翻转:一题从错到对(w)、一题从对到错(l)、一题不变。"""
|
||||
baseline = {"q1": False, "q2": True, "q3": True}
|
||||
candidate = {"q1": True, "q2": False, "q3": True}
|
||||
result = pair_block(baseline, candidate, ["q1", "q2", "q3"])
|
||||
assert result.w == 1
|
||||
assert result.l == 1
|
||||
assert result.observed == {
|
||||
"q1": (False, True),
|
||||
"q2": (True, False),
|
||||
"q3": (True, True),
|
||||
}
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""空输入应返回零翻转。"""
|
||||
result = pair_block({}, {}, [])
|
||||
assert result.w == 0 and result.l == 0
|
||||
|
||||
def test_all_wins(self) -> None:
|
||||
"""全部从错到对的极端情况。"""
|
||||
baseline = {"q1": False, "q2": False}
|
||||
candidate = {"q1": True, "q2": True}
|
||||
result = pair_block(baseline, candidate, ["q1", "q2"])
|
||||
assert result.w == 2
|
||||
assert result.l == 0
|
||||
|
||||
def test_all_losses(self) -> None:
|
||||
"""全部从对到错的极端情况。"""
|
||||
baseline = {"q1": True, "q2": True}
|
||||
candidate = {"q1": False, "q2": False}
|
||||
result = pair_block(baseline, candidate, ["q1", "q2"])
|
||||
assert result.w == 0
|
||||
assert result.l == 2
|
||||
|
||||
def test_subset_of_questions(self) -> None:
|
||||
"""只对 question_ids 中指定的子集进行比对。"""
|
||||
baseline = {"q1": False, "q2": True, "q3": True}
|
||||
candidate = {"q1": True, "q2": False, "q3": True}
|
||||
result = pair_block(baseline, candidate, ["q1"])
|
||||
assert result.w == 1
|
||||
assert result.l == 0
|
||||
assert "q2" not in result.observed
|
||||
|
||||
|
||||
class TestClassifyQuadrants:
|
||||
"""classify_quadrants 四象限分类测试。"""
|
||||
|
||||
def test_all_four(self) -> None:
|
||||
"""四个象限各有一题。"""
|
||||
observed = {
|
||||
"q1": (False, True),
|
||||
"q2": (True, False),
|
||||
"q3": (False, False),
|
||||
"q4": (True, True),
|
||||
}
|
||||
qc = classify_quadrants(observed)
|
||||
assert qc.improvements == ["q1"]
|
||||
assert qc.regressions == ["q2"]
|
||||
assert qc.persistent_fails == ["q3"]
|
||||
assert qc.stable_successes == ["q4"]
|
||||
|
||||
def test_sorted_within_quadrant(self) -> None:
|
||||
"""同象限内题目 ID 应按字典序排列。"""
|
||||
observed = {"z": (False, True), "a": (False, True)}
|
||||
qc = classify_quadrants(observed)
|
||||
assert qc.improvements == ["a", "z"]
|
||||
|
||||
def test_empty_observed(self) -> None:
|
||||
"""空输入应返回全空象限。"""
|
||||
qc = classify_quadrants({})
|
||||
assert qc.improvements == []
|
||||
assert qc.regressions == []
|
||||
assert qc.persistent_fails == []
|
||||
assert qc.stable_successes == []
|
||||
|
||||
|
||||
class TestComputeAccuracy:
|
||||
"""compute_accuracy 准确率计算测试。"""
|
||||
|
||||
def test_basic(self) -> None:
|
||||
"""一对一错,准确率 0.5。"""
|
||||
assert compute_accuracy({"q1": True, "q2": False}, ["q1", "q2"]) == 0.5
|
||||
|
||||
def test_all_correct(self) -> None:
|
||||
"""全部正确,准确率 1.0。"""
|
||||
assert compute_accuracy({"q1": True, "q2": True}, ["q1", "q2"]) == 1.0
|
||||
|
||||
def test_all_wrong(self) -> None:
|
||||
"""全部错误,准确率 0.0。"""
|
||||
assert compute_accuracy({"q1": False, "q2": False}, ["q1", "q2"]) == 0.0
|
||||
|
||||
def test_empty_raises(self) -> None:
|
||||
"""空题目列表应抛出 ZeroDivisionError。"""
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
compute_accuracy({}, [])
|
||||
Reference in New Issue
Block a user