whassan's picture
Deploy Sensorflow Studio (part 2)
e1c927b verified
Raw History Blame Contribute Delete
8.02 kB
"""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