File size: 8,015 Bytes
e1c927b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
204
205
206
207
208
"""Six-axis violation taxonomy, structured query, and clustering.

Axes: Actor / Vulnerability / Legality / Environment / Interaction /
Behavior, plus extensions RoadGeometry / TrafficControl / Visibility.

The structured query turns a natural-language request such as "failed to
yield to pedestrian at uncontrolled intersection during low visibility"
into a filterable query object via a DETERMINISTIC keyword map (no LLM in
the retrieval path), then filters violation signatures exactly.

Clustering mirrors raremine's mining semantics conceptually (signature ->
dedup -> diversity-aware exemplar) over ROTR-native structured signatures —
own implementation, raremine's store is untouched. Exact structured
matching over ~10^3 records needs a dict, not a vector DB (see the
DO-NOT-BUILD decision in the architecture doc).
"""

from __future__ import annotations

import hashlib
from typing import Dict, List, Optional

from pydantic import BaseModel

from sensorflow.rotr.models import ROTRScenario, ROTRViolation

TAXONOMY_VERSION = "rotr-taxonomy-1.0.0"

AXES = ["actor", "vulnerability", "legality", "environment", "interaction",
        "behavior", "road_geometry", "traffic_control", "visibility"]

RULE_TO_LEGALITY = {
    "R-YIELD-PED-01": "YIELD",
    "R-PATH-RESTRICT-02": "RESTRICTED_PATH",
    "R-LANE-MANEUVER-03": "LANE_ASSOCIATION",
    "R-INT-CONFLICT-04": "SIGNAL",
    "R-MERGE-GAP-05": "MERGE",
    "R-STOP-06": "STOP",
}

RULE_TO_BEHAVIOR = {
    "R-YIELD-PED-01": "proceed_without_yield",
    "R-PATH-RESTRICT-02": "enter_restricted_path",
    "R-LANE-MANEUVER-03": "unpermitted_maneuver",
    "R-INT-CONFLICT-04": "run_red_signal",
    "R-MERGE-GAP-05": "insufficient_gap_merge",
    "R-STOP-06": "rolling_stop",
}


def signature(violation: ROTRViolation, scenario: ROTRScenario) -> Dict[str, str]:
    material = [a for a in scenario.actors
                if a.actor_id in set(violation.actor_ids)]
    actor_cls = material[0].class_name if material else "none"
    vru = any(a.class_name in ("pedestrian", "cyclist") for a in material)
    interaction = "NONE"
    if material:
        intent = material[0].intent
        interaction = {"CROSSING": "CROSSING", "MERGING": "MERGING",
                       "PROCEEDING": "FOLLOWING"}.get(intent, "OTHER")
    env = scenario.environment
    ctx = scenario.actual_context
    return {
        "actor": actor_cls,
        "vulnerability": "VRU" if vru else "NON_VRU",
        "legality": RULE_TO_LEGALITY.get(violation.rule_id, "OTHER"),
        "environment": f"{env.lighting}/{env.weather}",
        "interaction": interaction,
        "behavior": RULE_TO_BEHAVIOR.get(violation.rule_id, "other"),
        "road_geometry": ctx.intersection_type,
        "traffic_control": ctx.control,
        "visibility": env.visibility,
    }


# ------------------------------------------------------------ structured query


class ROTRQuery(BaseModel):
    """Filterable query object over the taxonomy axes (all optional)."""

    actor: Optional[str] = None
    vulnerability: Optional[str] = None
    legality: Optional[str] = None
    environment: Optional[str] = None
    interaction: Optional[str] = None
    behavior: Optional[str] = None
    road_geometry: Optional[str] = None
    traffic_control: Optional[str] = None
    visibility: Optional[str] = None
    lighting: Optional[str] = None
    weather: Optional[str] = None
    consequence_class: Optional[str] = None
    primary_layer: Optional[str] = None
    text: Optional[str] = None            # original text, for provenance


# Deterministic keyword -> axis map (retrieval path is LLM-free).
_KEYWORDS = [
    ("pedestrian", "actor", "pedestrian"),
    ("cyclist", "actor", "cyclist"),
    ("vulnerable", "vulnerability", "VRU"),
    ("yield", "legality", "YIELD"),
    ("restricted", "legality", "RESTRICTED_PATH"),
    ("bus lane", "legality", "RESTRICTED_PATH"),
    ("wrong lane", "legality", "LANE_ASSOCIATION"),
    ("lane association", "legality", "LANE_ASSOCIATION"),
    ("red light", "legality", "SIGNAL"),
    ("signal", "legality", "SIGNAL"),
    ("merge", "legality", "MERGE"),
    ("stop sign", "legality", "STOP"),
    ("rolling stop", "legality", "STOP"),
    ("uncontrolled intersection", "road_geometry", "uncontrolled"),
    ("controlled intersection", "road_geometry", "controlled"),
    ("intersection", "road_geometry", None),   # weak; only if none set
    ("low visibility", "visibility", "low"),
    ("night", "lighting", "night"),
    ("dusk", "lighting", "dusk"),
    ("rain", "weather", "rain"),
    ("crossing", "interaction", "CROSSING"),
    ("safety critical", "consequence_class", "SAFETY_CRITICAL"),
]


def parse_query(text: str) -> ROTRQuery:
    low = text.lower()
    fields: Dict[str, str] = {}
    for kw, axis, value in _KEYWORDS:
        if kw in low and value is not None and axis not in fields:
            fields[axis] = value
    return ROTRQuery(text=text, **fields)


def matches(query: ROTRQuery, sig: Dict[str, str],
            consequence_class: Optional[str],
            primary_layer: Optional[str],
            environment: Dict[str, str]) -> bool:
    for axis in AXES:
        want = getattr(query, axis)
        if want is not None and sig.get(axis) != want:
            return False
    if query.lighting is not None and environment.get("lighting") != query.lighting:
        return False
    if query.weather is not None and environment.get("weather") != query.weather:
        return False
    if query.consequence_class is not None and \
            consequence_class != query.consequence_class:
        return False
    if query.primary_layer is not None and primary_layer != query.primary_layer:
        return False
    return True


# ------------------------------------------------------------ clustering


CLUSTER_KEY_AXES = ["legality", "actor", "traffic_control", "behavior"]


def cluster_key(sig: Dict[str, str], primary_layer: Optional[str]) -> str:
    parts = [sig.get(a, "?") for a in CLUSTER_KEY_AXES] + [primary_layer or "?"]
    return "|".join(parts)


def cluster_id_for(key: str) -> str:
    return "cl-" + hashlib.sha256(
        (TAXONOMY_VERSION + "|" + key).encode()).hexdigest()[:8]


def build_clusters(items: List[Dict]) -> List[Dict]:
    """items: [{violation_id, signature, primary_layer, consequence_class,
    environment}] -> recurring-pattern clusters with a diversity-aware
    exemplar (max environment spread, raremine-style selection concept)."""
    groups: Dict[str, List[Dict]] = {}
    for it in items:
        key = cluster_key(it["signature"], it.get("primary_layer"))
        groups.setdefault(key, []).append(it)
    clusters = []
    for key, members in sorted(groups.items()):
        envs = sorted({m["signature"].get("environment", "") +
                       "/" + m["signature"].get("visibility", "")
                       for m in members})
        cons: Dict[str, int] = {}
        for m in members:
            c = m.get("consequence_class") or "UNCLASSIFIED"
            cons[c] = cons.get(c, 0) + 1
        # Exemplar: the member in the least-represented environment cell
        # (diversity-aware pick, not just the first).
        env_counts: Dict[str, int] = {}
        for m in members:
            e = m["signature"].get("environment", "")
            env_counts[e] = env_counts.get(e, 0) + 1
        exemplar = min(members,
                       key=lambda m: (env_counts[m["signature"].get("environment", "")],
                                      m["violation_id"]))
        clusters.append({
            "cluster_id": cluster_id_for(key),
            "key": key,
            "count": len(members),
            "member_violation_ids": [m["violation_id"] for m in members],
            "exemplar_violation_id": exemplar["violation_id"],
            "environment_spread": envs,
            "consequence_distribution": cons,
            "taxonomy_version": TAXONOMY_VERSION,
        })
    clusters.sort(key=lambda c: -c["count"])
    return clusters