Files
Video-Tree-TRM5/tests/unit/test_correctness_unit_view.py
T
iomgaa 4b6d1d8a50 feat(harness): correctness 三口径 + gate 块按 unit 跑
进化引擎与 gate e-process 从 question_id 口径迁至 unit_id 口径,AR pair
双向 AND 折叠为单元、不被 P/Q 单题计分污染;逐题 predictions 仅作溯源。

- question_units: 新增 unit_correctness_view(units, per_q)->dict[unit_id,bool]
  作为逐题→单元折叠的唯一入口(复用 unit_correctness)。
- core/evolution/validate: pair_block/compute_accuracy 参数改 unit_ids、
  分母按单元数(键即 unit_id)。
- app/harness/validate(gate 实际执行路径):阶梯题序聚合为单元并保持信息
  阶梯序(_ladder_units),gate 块按单元切分(AR pair 整锁不跨块拆);
  baseline_cache 键含 unit_id、存单元级对错;候选逐题读回后折叠成单元视图;
  n_used/W/L/四象限/准确率均按单元计;证据行按 unit 口径,candidate_correctness
  独立保留逐题对错供 runner 二轨合并。
- runner: probation 结算按 unit 折叠计 W/L(_probation_unit_flips);quadrant
  四象限 id 承载 unit_id。

核心算法保真 #5(信息阶梯 e-process):本次仅迁移 correctness 口径,不改冷启动
2:1 / gamma-EMA / 反泄漏算法本身(gate_ladder 迁移见 Task 8)。
2026-07-15 07:31:03 -04:00

132 lines
5.2 KiB
Python
Raw 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.
"""tests/unit/test_correctness_unit_view.py — correctness 三对象口径单元测试。
验证 unit_correctness_view(逐题 per_q → unit 折叠:AR pair 双向 AND、非 AR single
及 core.evolution 的 pair_block / compute_accuracy 消费 unit 口径时,混格池中
AR pair 折叠为单元、W/L 与准确率分母不被 P/Q 单题计分污染(核心算法保真 #5)。
"""
from __future__ import annotations
from app.harness.question_units import build_units, unit_correctness_view
from core.evolution.validate import classify_quadrants, compute_accuracy, pair_block
from core.types import GeneratedQuestion
def _single(qid: str, task_type: str = "temporal") -> GeneratedQuestion:
"""构造一条非 AR single 题(unit_id 回填为 question_id)。"""
return GeneratedQuestion(
question_id=qid,
video_id=f"v_{qid}",
task_type=task_type,
question="Q?",
options=("A", "B", "C", "D"),
answer="A",
source_nodes=(),
difficulty="easy",
)
def _pair(pair_id: str, task_type: str = "temporal") -> tuple[GeneratedQuestion, GeneratedQuestion]:
"""构造一个 AR 孪生对(original + mirror,共享 pair_id → unit_id=pair_id)。"""
common = {
"video_id": f"v_{pair_id}",
"task_type": task_type,
"question": "Q?",
"options": ("A", "B", "C", "D"),
"answer": "A",
"source_nodes": (),
"difficulty": "easy",
"pair_id": pair_id,
"flip_axis": "before_after",
}
orig = GeneratedQuestion(question_id=f"{pair_id}_o", question_role="pair_original", **common)
mirror = GeneratedQuestion(question_id=f"{pair_id}_m", question_role="pair_mirror", **common)
return orig, mirror
class TestUnitCorrectnessView:
"""unit_correctness_view:逐题对错折叠成 unit_id → bool。"""
def test_mixed_pool_and_semantics(self) -> None:
"""混格:single 直取、AR pair 双向 AND。"""
s0 = _single("s0")
s1 = _single("s1")
p1o, p1m = _pair("p1")
p2o, p2m = _pair("p2")
units = build_units([s0, s1, p1o, p1m, p2o, p2m])
per_q = {
"s0": True,
"s1": False,
"p1_o": True,
"p1_m": True,
"p2_o": True,
"p2_m": False,
}
view = unit_correctness_view(units, per_q)
# single 的 unit_id 等于 question_idpair 的 unit_id 等于 pair_id
assert view == {"s0": True, "s1": False, "p1": True, "p2": False}
def test_missing_per_q_raises(self) -> None:
"""任一题缺 per_q → KeyError(禁静默兜底)。"""
p1o, p1m = _pair("p1")
units = build_units([p1o, p1m])
import pytest
with pytest.raises(KeyError):
unit_correctness_view(units, {"p1_o": True})
class TestPairBlockUnitFold:
"""pair_block 消费 unit 口径:混格 W/L 不被 P/Q 单题计分污染。"""
def test_pair_partial_improvement_not_counted(self) -> None:
"""AR pair 基线(F,F)→候选(T,F):单元仍错,不计 W(保真 #5)。"""
p1o, p1m = _pair("p1")
s0 = _single("s0")
units = build_units([p1o, p1m, s0])
unit_ids = [u.unit_id for u in units]
baseline_per_q = {"p1_o": False, "p1_m": False, "s0": False}
candidate_per_q = {"p1_o": True, "p1_m": False, "s0": True}
b_units = unit_correctness_view(units, baseline_per_q)
c_units = unit_correctness_view(units, candidate_per_q)
result = pair_block(b_units, c_units, unit_ids)
# 只有 s0 单元发生 F→T 翻转;pair 单元双向 AND 后仍错,不计 W
assert result.w == 1
assert result.l == 0
assert result.observed["p1"] == (False, False)
assert result.observed["s0"] == (False, True)
def test_pair_full_flip_counts_once(self) -> None:
"""AR pair 两题齐翻(F,F)→(T,T):单元计 1 次 W(非 2)。"""
p1o, p1m = _pair("p1")
units = build_units([p1o, p1m])
b_units = unit_correctness_view(units, {"p1_o": False, "p1_m": False})
c_units = unit_correctness_view(units, {"p1_o": True, "p1_m": True})
result = pair_block(b_units, c_units, [u.unit_id for u in units])
assert result.w == 1
assert result.l == 0
class TestComputeAccuracyUnitDenominator:
"""compute_accuracy 分母按 unit 数(非逐题)。"""
def test_denominator_is_unit_count(self) -> None:
"""1 pair(错) + 1 single(对) → 1/2;逐题会误算 2/3。"""
p1o, p1m = _pair("p1")
s0 = _single("s0")
units = build_units([p1o, p1m, s0])
unit_ids = [u.unit_id for u in units]
view = unit_correctness_view(units, {"p1_o": True, "p1_m": False, "s0": True})
assert compute_accuracy(view, unit_ids) == 0.5
class TestClassifyQuadrantsUnitKeys:
"""classify_quadrants 按 unit_id 分桶(pair 单元只出现一次)。"""
def test_pair_unit_single_bucket(self) -> None:
"""pair 单元 F→F 落 persistent_fails,只记 unit_id 一次。"""
observed = {"p1": (False, False), "s0": (False, True)}
qc = classify_quadrants(observed)
assert qc.improvements == ["s0"]
assert qc.persistent_fails == ["p1"]