File size: 5,344 Bytes
bb59211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""全零占位位姿 —— 一个把整集假判成 FAIL 的 bug 的回归锁。

bug 长什么样
------------
``observation.slam_pose`` 里没采到的帧写的是**全零占位** ``(0,0,0,0,0,0,0)``。
它长得完全像合法位姿:长度 7、能索引、能相减。但它的四元数模是 0,于是

    两个零四元数的点积 = 0
    acos(0) = π/2  →  夹角 = 2 × π/2 = **π = 180°**
    30fps 下角速度 = π × 30 = **94.248 rad/s**

**任何涉及零占位的相邻对都会报一次"180° 跳变"** —— 不只是"真实↔零"的过渡,
两个零之间也一样。

实测 Test94 的 slam_pose 有 437/599 行是全零,SLAM 连续性因此报出 **465 处**
"跳变"(599 帧里几乎每一帧),整集被判 FAIL → **拦回人工审核**。而峰值角速度
94.248 就是上面那个 π×30,是识别这个 bug 的指纹。

    pytest tests/test_cleaning_pose_validity.py
    python3 tests/test_cleaning_pose_validity.py
"""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))  # noqa: E402

from app.processing.cleaning import collect  # noqa: E402
from app.processing.cleaning.checks import (  # noqa: E402
    CheckContext, StreamEvidence, get_check, PASS, WARN, FAIL,
)
from app.processing.cleaning.contract import pose_is_valid  # noqa: E402

# π × 30fps —— 零四元数产生的"每帧 180°"角速度
ZERO_QUAT_RPS = 94.248

# 一个模不为 0 的合法朝向
VALID = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
ZERO = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)


def _context(poses, fps=30.0):
    stream = StreamEvidence(key="slam_pose", channel="slam", fps=fps,
                            series={"pose": tuple(poses)})
    return CheckContext(batch_dir=Path("/nonexistent"), fps=fps, streams=(stream,))


def _continuity(poses, fps=30.0):
    ctx = _context(poses, fps)
    ctx.params = {}
    findings = get_check("umi.slam_continuity").run(ctx)
    assert findings, "一条结论都没产出"
    return findings[0]


# ── 判据本身 ────────────────────────────────────────────────
def test_zero_quaternion_is_invalid():
    assert pose_is_valid(VALID) is True
    assert pose_is_valid(ZERO) is False          # ← 核心
    assert pose_is_valid(()) is False            # 空元组本来就不算
    assert pose_is_valid((1.0, 2.0, 3.0)) is False


def test_zero_position_is_still_a_valid_pose():
    """位置可以合法地是原点;只有四元数模为 0 才不是合法朝向。"""
    assert pose_is_valid((0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)) is True


# ── 核心回归 ────────────────────────────────────────────────
def test_zero_placeholders_do_not_create_jumps():
    """★ 全是零占位的一集不该报任何跳变。"""
    finding = _continuity([ZERO] * 500)
    assert finding.status is PASS, f"零占位被当成了跳变:{finding.message}"
    assert finding.metrics["jumps"] == 0


def test_mixed_valid_and_zero_placeholders_do_not_create_jumps():
    """★ 真实↔零占位的过渡同样不该报 —— 这正是 Test94 的情况。"""
    poses = []
    for index in range(200):
        poses.append(VALID if index % 3 == 0 else ZERO)
    finding = _continuity(poses)
    assert finding.status is PASS, f"混合占位报出 {finding.metrics['jumps']} 处跳变"
    assert finding.metrics["jumps"] == 0


def test_zero_placeholder_would_have_produced_the_94_rps_signature():
    """把守卫去掉,峰值角速度应当落回 94.248 —— 确认这确实是那个 bug。"""
    finding = _continuity([ZERO] * 100)
    assert finding.metrics["peak_angular_rps"] < ZERO_QUAT_RPS / 10


# ── 真阳性不能被一起修掉 ─────────────────────────────────────
def test_real_relocation_jump_is_still_detected():
    """两帧合法位姿之间突然位移 → 仍要报出来。"""
    near = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
    far = (10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)      # 10 米瞬移
    finding = _continuity([near] * 50 + [far] * 50)
    assert finding.status is WARN
    assert finding.metrics["jumps"] == 1
    assert finding.metrics["peak_linear_mps"] > 100   # 10m / (1/30s) = 300 m/s


def test_many_real_jumps_still_fail():
    near = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
    far = (5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
    poses = []
    for _ in range(30):
        poses.extend([near, far])
    finding = _continuity(poses)
    assert finding.status is FAIL
    assert finding.metrics["jumps"] >= 10


# ── 采集层的 valid_count 也要认这个 ─────────────────────────
def test_collector_valid_count_excludes_zero_placeholders():
    """位姿流的 valid_count 若把零占位算作有效,别的检查也会被带偏。"""
    values = [list(VALID), list(ZERO), None, list(VALID)]
    counts = tuple(
        1 if collect.pose_is_valid(tuple(item)) else 0
        for item in collect.vector_series(values, 7))
    assert counts == (1, 0, 0, 1)


if __name__ == "__main__":
    import pytest
    raise SystemExit(pytest.main([__file__, "-q"]))