File size: 7,584 Bytes
729628a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""Every injected synthetic error must be caught by the appropriate subsystem."""

import numpy as np
import pytest

from sensorflow.evaluation import reporting, synthetic
from sensorflow.evaluation.pipeline import reset_pipeline
from sensorflow.evaluation.records import reset_store


@pytest.fixture(scope="module")
def env(tmp_path_factory):
    store = reset_store(tmp_path_factory.mktemp("labeleval"))
    pipe = reset_pipeline(store)
    ds = synthetic.generate_dataset(store, num_sequences=4, frames_per_sequence=25, seed=7)
    pipe.run(ds.dataset_id, background=False)
    anns = store.where("annotations", dataset_id=ds.dataset_id)
    return store, pipe, ds, anns


def _with_error(anns, err):
    return [a for a in anns if err in a.injected_errors]


def _reasons(store, ann):
    d = store.get("triage_decisions", ann.annotation_id)
    return set(d.failure_reasons) if d else set()


def _detection_rate(store, anns, err, expected_reasons):
    subset = _with_error(anns, err)
    assert subset, f"generator injected no {err}"
    caught = [a for a in subset
              if a.status == "FLAGGED" and (_reasons(store, a) & expected_reasons)]
    return len(caught) / len(subset), subset


# ------------------------------------------------------------------ per-error assertions


def test_all_injected_errors_are_flagged(env):
    store, _, _, anns = env
    injected = [a for a in anns if a.injected_errors]
    flagged = [a for a in injected if a.status == "FLAGGED"]
    assert len(flagged) / len(injected) >= 0.95


def test_false_positive_caught(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "FALSE_POSITIVE", {"ANOMALY", "LOW_IOU", "INSUFFICIENT_POINT_SUPPORT"})
    assert rate >= 0.9


def test_false_negative_visible_as_missed_gt(env):
    store, _, ds, anns = env
    n_injected_fn = ds.generation_params["injected"]["FALSE_NEGATIVE"]
    assert n_injected_fn > 0
    # Missed objects surface as false_negative points in the haystack.
    points = reporting.haystack(store, ds.dataset_id)
    fn_points = [p for p in points if p["category"] == "false_negative"]
    assert len(fn_points) >= n_injected_fn * 0.9
    # And recall < 1 because references exist.
    m = reporting.quality_metrics(store, ds.dataset_id)["global"]
    assert m["recall"] is not None and m["recall"] < 1.0


def test_bad_3d_box_caught_by_validation(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "BAD_3D_BOX", {"LOW_IOU", "INSUFFICIENT_POINT_SUPPORT"})
    assert rate >= 0.9


def test_wrong_orientation_caught(env):
    store, _, _, anns = env
    rate, subset = _detection_rate(store, anns, "WRONG_ORIENTATION", {"ORIENTATION_ERROR", "LOW_IOU"})
    assert rate >= 0.9
    # Orientation error must be measured, not inferred.
    v = store.get("validations", subset[0].annotation_id)
    assert v.orientation_error_deg is None or v.orientation_error_deg > 10


def test_wrong_position_caught(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "WRONG_POSITION",
                              {"POSITION_ERROR", "LOW_IOU", "INSUFFICIENT_POINT_SUPPORT"})
    assert rate >= 0.9


def test_id_switch_caught_by_tracking(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "ID_SWITCH", {"ID_SWITCH"})
    assert rate >= 0.9


def test_track_fragmentation_caught(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "TRACK_FRAGMENTATION", {"TRACK_FRAGMENTATION", "ID_SWITCH"})
    assert rate >= 0.9


def test_low_point_density_caught(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "LOW_POINT_DENSITY", {"INSUFFICIENT_POINT_SUPPORT"})
    assert rate >= 0.9


def test_sensor_disagreement_caught(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "SENSOR_DISAGREEMENT", {"SENSOR_DISAGREEMENT"})
    assert rate >= 0.9


def test_grader_disagreement_caught(env):
    store, _, _, anns = env
    rate, subset = _detection_rate(store, anns, "GRADER_DISAGREEMENT", {"GRADER_DISAGREEMENT"})
    assert rate >= 0.9
    g = store.get("grader_comparisons", subset[0].annotation_id)
    assert g.consensus < 0.9 and "classification" in g.disagreement_types


def test_low_confidence_caught(env):
    store, _, _, anns = env
    rate, _ = _detection_rate(store, anns, "LOW_CONFIDENCE", {"LOW_CONFIDENCE"})
    assert rate >= 0.9


def test_clean_labels_mostly_auto_verified(env):
    store, _, _, anns = env
    clean = [a for a in anns if not a.injected_errors]
    verified = [a for a in clean if a.status == "VERIFIED"]
    assert len(verified) / len(clean) >= 0.8


# ------------------------------------------------------------------ rare events


def test_rare_events_detected_with_evidence(env):
    store, _, ds, _ = env
    events = store.where("rare_events", dataset_id=ds.dataset_id)
    types = {e.scenario_type for e in events}
    assert "near_collision" in types
    assert "sensor_failure" in types
    assert "vru_interaction" in types
    for e in events:
        assert e.evidence_frames and e.sensor_evidence
        assert 0 <= e.rarity_score <= 1


# ------------------------------------------------------------------ anomaly evidence retention


def test_anomaly_decisions_retain_full_evidence(env):
    store, _, _, anns = env
    an = store.get("anomalies", anns[0].annotation_id)
    assert an.detector_scores and an.normalized_scores
    assert set(an.detector_scores) == set(an.normalized_scores)
    assert an.ensemble_strategy in ("majority_vote", "weighted_average", "meta_classifier")
    assert 0 < an.decision_threshold <= 1


# ------------------------------------------------------------------ triage explainability


def test_every_decision_has_policy_and_gate_lines(env):
    store, _, _, anns = env
    for a in anns[:50]:
        d = store.get("triage_decisions", a.annotation_id)
        assert d is not None
        assert d.policy_id and d.policy_values
        assert d.gate_lines, "explainable per-gate lines required"
        if d.status == "FLAGGED":
            assert d.primary_failure_reason is not None
            failing = [l for l in d.gate_lines if l.applicable and not l.passed]
            assert failing, "flagged decisions must show a failing gate"


# ------------------------------------------------------------------ model regression


def test_model_regression_detected_on_degraded_second_run(tmp_path):
    store = reset_store(tmp_path)
    pipe = reset_pipeline(store)

    ds1 = synthetic.generate_dataset(store, num_sequences=3, frames_per_sequence=20, seed=21)
    pipe.run(ds1.dataset_id, background=False)
    first = [r for r in store.all("regressions")]
    assert first and not first[-1].regression_detected  # no baseline yet

    # Second dataset labeled by a degraded model: pedestrian recall collapses.
    ds2 = synthetic.generate_dataset(store, name="degraded", num_sequences=3,
                                     frames_per_sequence=20, seed=22)
    synthetic.generate_labels(store, ds2, model_version="model-v2-degraded",
                              degrade_classes=["pedestrian", "vehicle"], seed_offset=5)
    pipe.run(ds2.dataset_id, background=False)

    regs = sorted(store.all("regressions"), key=lambda r: r.date)
    latest = regs[-1]
    assert latest.regression_detected
    assert "performance" in latest.kinds
    regressed_metrics = {d.metric for d in latest.deltas if d.regressed}
    assert "recall" in regressed_metrics or "safety_critical_recall" in regressed_metrics
    assert pipe.regression_alert