Spaces:
Paused
Paused
Download sensorflow/safety/semantic.py from whassan/SensorflowStudio: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/safety/semantic.py
- Command line
-
hf download hf://spaces/whassan/SensorflowStudio/sensorflow/safety/semantic.py
-
curl -L -o semantic.py https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/safety/semantic.py
13.6 kB
| """Neuro-symbolic semantic scenario mining (lightweight, two-stage). | |
| Industry concept: natural-language scenario search over fleet data (Voxel51 / | |
| Scale Nucleus semantic search, Applied Intuition scenario mining). Two stages: | |
| 1. SYMBOLIC filter — structured attributes (container ODD dimensions, error | |
| rates, dynamics) restrict the candidate set exactly. | |
| 2. REASONING scorer — if a local Ollama endpoint is reachable (reusing the | |
| copilot plumbing in sensorflow/evaluation/copilot.py) it contributes a | |
| natural-language rationale for the top candidates; otherwise (and always, | |
| for the actual ranking) a DETERMINISTIC rule-based scorer maps concept | |
| keywords to structured evidence via an explicit lexicon. Every result | |
| carries per-stage explanations of why it matched. | |
| Hybrid retrieval: the rule score is blended with cosine similarity over the | |
| run's existing 32-d structural container embeddings (centroid of the top rule | |
| matches acts as the query anchor), combining symbolic and dense signals. | |
| Honest marker: the "reasoning" stage is a keyword lexicon over structured | |
| evidence unless Ollama is reachable; rankings are deterministic either way | |
| (the LLM adds rationale text, it never reorders results). | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Dict, List, Optional, Tuple | |
| import numpy as np | |
| from sensorflow.megaeval import population as pop_mod | |
| from sensorflow.megaeval.population import DIMENSIONS | |
| # Each lexicon entry: (regex over the concept text, human-readable meaning, | |
| # scorer(row) -> (score in [0,1], evidence string)). | |
| _LEXICON: List[Tuple[str, str, object]] = [] | |
| def _rule(pattern: str, meaning: str): | |
| def deco(fn): | |
| _LEXICON.append((pattern, meaning, fn)) | |
| return fn | |
| return deco | |
| def _vru(row): | |
| share = row["safety_n"] / max(row["n_objects"], 1) | |
| return min(1.0, share * 4.0), f"safety-critical VRU share {share:.0%}" | |
| def _night(row): | |
| if row["lighting"] == "night": | |
| return 1.0, "lighting=night" | |
| if row["lighting"] == "dusk": | |
| return 0.5, "lighting=dusk" | |
| return 0.0, "" | |
| def _weather(row): | |
| return (1.0, f"weather={row['weather']}") if row["weather"] != "clear" else (0.0, "") | |
| def _anom(row): | |
| rate = row["anomalies"] / max(row["n_objects"], 1) | |
| return min(1.0, rate * 8.0), f"anomaly rate {rate:.0%}" | |
| def _occl(row): | |
| return (1.0, "scenario=occlusion_heavy") if row["scenario"] == "occlusion_heavy" else (0.0, "") | |
| def _nearmiss(row): | |
| if row["scenario"] in ("near_miss", "extreme_ttc"): | |
| return 1.0, f"scenario={row['scenario']}" | |
| return 0.0, "" | |
| def _intersection(row): | |
| return (1.0, "road_type=intersection") if row["road_type"] == "intersection" else (0.0, "") | |
| def _urban(row): | |
| return (1.0, "road_type=urban") if row["road_type"] == "urban" else (0.0, "") | |
| def _highway(row): | |
| return (1.0, "road_type=highway") if row["road_type"] == "highway" else (0.0, "") | |
| def _rural(row): | |
| return (1.0, "road_type=rural") if row["road_type"] == "rural" else (0.0, "") | |
| def _lane(row): | |
| # Proxy: the population has no lane geometry; urban/intersection scenes are | |
| # where lane-edge interactions concentrate. Explained as a proxy. | |
| if row["road_type"] in ("urban", "intersection"): | |
| return 0.5, f"road_type={row['road_type']} (lane-edge proxy — no lane geometry instrumented)" | |
| return 0.0, "" | |
| def _fn(row): | |
| rate = row["fn"] / max(row["n_objects"], 1) | |
| return min(1.0, rate * 5.0), f"FN rate {rate:.0%}" | |
| def _fp(row): | |
| rate = row["fp"] / max(row["n_objects"], 1) | |
| return min(1.0, rate * 5.0), f"FP rate {rate:.0%}" | |
| def _sensor(row): | |
| return (1.0, "scenario=sensor_degraded") if row["scenario"] == "sensor_degraded" else (0.0, "") | |
| def _risk(row): | |
| return float(row["risk_score"]), f"risk_score {row['risk_score']:.2f}" | |
| def _container_rows(store, run) -> List[Dict]: | |
| art = store.artifacts(run.run_id) | |
| df = art["containers"] | |
| rows = [] | |
| for _, r in df.iterrows(): | |
| rows.append({ | |
| "container_id": int(r["container_id"]), | |
| **{dim: DIMENSIONS[dim][int(r[dim])] for dim in pop_mod.CONTAINER_DIMS}, | |
| "n_objects": int(r["n_objects"]), | |
| "fn": int(r["fn"]), "fp": int(r["fp"]), | |
| "anomalies": int(r["anomalies"]), | |
| "safety_n": int(r["safety_n"]), | |
| "risk_score": float(r["risk_score"]), | |
| }) | |
| return rows | |
| def _symbolic_filter(rows: List[Dict], filters: Optional[Dict]) -> Tuple[List[Dict], Dict]: | |
| before = len(rows) | |
| applied = {} | |
| for dim, values in (filters or {}).items(): | |
| if dim not in pop_mod.CONTAINER_DIMS: | |
| continue | |
| vals = values if isinstance(values, list) else [values] | |
| rows = [r for r in rows if r[dim] in vals] | |
| applied[dim] = vals | |
| return rows, {"stage": "symbolic_filter", "applied_filters": applied, | |
| "candidates_before": before, "candidates_after": len(rows)} | |
| def _rule_score(concept: str, row: Dict) -> Tuple[float, List[Dict]]: | |
| matched = [] | |
| total = 0.0 | |
| weight_sum = 0.0 | |
| for pattern, meaning, fn in _LEXICON: | |
| if not re.search(pattern, concept, flags=re.IGNORECASE): | |
| continue | |
| score, evidence = fn(row) | |
| weight_sum += 1.0 | |
| total += score | |
| if score > 0: | |
| matched.append({"term": pattern, "meaning": meaning, | |
| "evidence": evidence, "contribution": round(score, 4)}) | |
| if weight_sum == 0: | |
| return float(row["risk_score"]), [{ | |
| "term": "(fallback)", "meaning": "no lexicon term matched the concept", | |
| "evidence": f"ranked by composite risk_score {row['risk_score']:.2f}", | |
| "contribution": round(float(row["risk_score"]), 4)}] | |
| return total / weight_sum, matched | |
| def _llm_rationale(concept: str, top_rows: List[Dict], timeout_s: float = 2.0) -> Optional[Dict]: | |
| """Optional Ollama pass (copilot plumbing); never reorders results.""" | |
| try: | |
| import json as _json | |
| import httpx | |
| from sensorflow.evaluation.copilot import OLLAMA_ENDPOINTS | |
| prompt = ( | |
| "You are a scenario-mining assistant. Concept query: " | |
| f"{concept!r}. Candidate scenes (structured evidence):\n" | |
| f"{_json.dumps(top_rows[:6], indent=1)[:2500]}\n" | |
| "In 3 sentences, explain which candidates best match the concept and why." | |
| ) | |
| for ep in OLLAMA_ENDPOINTS: | |
| try: | |
| res = httpx.post(ep["url"], json={ | |
| "model": ep["model"], | |
| "messages": [{"role": "user", "content": prompt}], | |
| "stream": False, | |
| }, timeout=timeout_s) | |
| if res.status_code == 200: | |
| text = res.json().get("message", {}).get("content", "") | |
| if text: | |
| return {"provider": ep["url"], "rationale": text} | |
| except Exception: | |
| continue | |
| except Exception: | |
| pass | |
| return None | |
| def search_containers(store, run, concept: str, filters: Optional[Dict] = None, | |
| k: int = 12, use_llm: Optional[bool] = None) -> Dict: | |
| """Two-stage neuro-symbolic search over a run's containers. | |
| Ranking = 0.65 * deterministic rule score + 0.35 * embedding similarity to | |
| the centroid of the top rule matches (hybrid retrieval). LLM (if enabled | |
| and reachable) adds rationale text only. | |
| """ | |
| rows = _container_rows(store, run) | |
| rows, stage1 = _symbolic_filter(rows, filters) | |
| scored = [] | |
| for row in rows: | |
| score, matched = _rule_score(concept, row) | |
| scored.append((score, row, matched)) | |
| scored.sort(key=lambda t: (-t[0], t[1]["container_id"])) | |
| # Hybrid: embedding similarity to the centroid of the top rule matches. | |
| art = store.artifacts(run.run_id) | |
| emb_ids, emb = art.get("emb_ids"), art.get("emb") | |
| emb_sim: Dict[int, float] = {} | |
| if emb is not None and scored: | |
| anchor_ids = [t[1]["container_id"] for t in scored[:10] if t[0] > 0] | |
| pos = np.where(np.isin(emb_ids, anchor_ids))[0] | |
| if pos.size: | |
| centroid = emb[pos].mean(axis=0) | |
| norm = np.linalg.norm(centroid) | |
| if norm > 1e-9: | |
| sims = emb @ (centroid / norm) | |
| emb_sim = {int(cid): float(s) for cid, s in zip(emb_ids, sims)} | |
| results = [] | |
| for rule_score, row, matched in scored: | |
| sim = emb_sim.get(row["container_id"], 0.0) | |
| sim01 = (sim + 1.0) / 2.0 | |
| final = 0.65 * rule_score + 0.35 * sim01 if emb_sim else rule_score | |
| results.append({ | |
| **row, | |
| "score": round(final, 4), | |
| "rule_score": round(rule_score, 4), | |
| "embedding_similarity": round(sim, 4) if emb_sim else None, | |
| "explanations": { | |
| "stage1_symbolic": ("passed structured filters" | |
| if stage1["applied_filters"] else "no filters applied"), | |
| "stage2_reasoning": matched, | |
| }, | |
| }) | |
| results.sort(key=lambda r: (-r["score"], r["container_id"])) | |
| results = results[:max(1, min(k, 100))] | |
| llm = None | |
| if use_llm or use_llm is None: | |
| # auto mode: short-timeout attempt; deterministic ranking is unaffected | |
| if use_llm: # explicit request gets a slightly longer timeout | |
| llm = _llm_rationale(concept, results, timeout_s=6.0) | |
| else: | |
| llm = _llm_rationale(concept, results, timeout_s=1.5) | |
| return { | |
| "concept": concept, | |
| "run_id": run.run_id, | |
| "target": "containers", | |
| "stage1": stage1, | |
| "stage2": { | |
| "stage": "reasoning_scorer", | |
| "provider": (llm or {}).get("provider", "offline_deterministic"), | |
| "note": "deterministic lexicon over structured evidence; LLM (when " | |
| "reachable) adds rationale text and never reorders results", | |
| }, | |
| "hybrid": {"rule_weight": 0.65, "embedding_weight": 0.35, | |
| "embedding_source": "32-d structural container embeddings"}, | |
| "llm_rationale": (llm or {}).get("rationale"), | |
| "results": results, | |
| } | |
| def search_scenarios(concept: str, filters: Optional[Dict] = None, k: int = 12) -> Dict: | |
| """Same two-stage idea over the scenario database (odd_tags as attributes).""" | |
| from sensorflow.safety.scenario_db import get_db | |
| records = get_db().search(limit=1000, odd_tags=None) | |
| stage1_before = len(records) | |
| filters = filters or {} | |
| for key, val in filters.items(): | |
| vals = val if isinstance(val, list) else [val] | |
| records = [r for r in records | |
| if r.odd_tags.get(key) in vals or getattr(r, key, None) in vals] | |
| results = [] | |
| for r in records: | |
| row = { # adapt scenario record to the lexicon's row shape | |
| "lighting": r.odd_tags.get("lighting", ""), | |
| "weather": r.odd_tags.get("weather", ""), | |
| "road_type": r.odd_tags.get("road_type", ""), | |
| "scenario": r.scenario_type, | |
| "n_objects": 1, "fn": 0, "fp": 0, "anomalies": 0, | |
| "safety_n": 1 if r.odd_tags.get("actor_class") in | |
| ("pedestrian", "cyclist", "motorcycle") else 0, | |
| "risk_score": {"low": 0.2, "medium": 0.45, "high": 0.7, | |
| "critical": 0.95}.get(r.severity, 0.4), | |
| } | |
| score, matched = _rule_score(concept, row) | |
| text_bonus = 0.15 if re.search(r"\w", concept) and any( | |
| w in r.description.lower() for w in re.findall(r"[a-z]{4,}", concept.lower())) else 0.0 | |
| results.append({ | |
| "scenario_id": r.scenario_id, | |
| "scenario_type": r.scenario_type, | |
| "source": r.source, | |
| "severity": r.severity, | |
| "odd_tags": r.odd_tags, | |
| "description": r.description, | |
| "score": round(min(1.0, score + text_bonus), 4), | |
| "explanations": {"stage1_symbolic": filters or "no filters applied", | |
| "stage2_reasoning": matched}, | |
| }) | |
| results.sort(key=lambda x: (-x["score"], x["scenario_id"])) | |
| return { | |
| "concept": concept, | |
| "target": "scenarios", | |
| "stage1": {"stage": "symbolic_filter", "applied_filters": filters, | |
| "candidates_before": stage1_before, "candidates_after": len(results)}, | |
| "stage2": {"stage": "reasoning_scorer", "provider": "offline_deterministic"}, | |
| "results": results[:max(1, min(k, 100))], | |
| } | |