Spaces:
Paused
Paused
Download sensorflow/rotr/scenes.py from whassan/SensorflowStudio: direct link, hf CLI and curl.
- Browser
- Download file 22.7 kB
-
https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/rotr/scenes.py
- Command line
-
hf download hf://spaces/whassan/SensorflowStudio/sensorflow/rotr/scenes.py
-
curl -L -o scenes.py https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/rotr/scenes.py
22.7 kB
| """Deterministic road-context scenario substrate for ROTR. | |
| Extends the platform's scene conventions (REUSE, read-only: | |
| bevfusion.scenes DT / EGO_SPEED / seeded default_rng discipline) with what | |
| bevfusion deliberately does not model: lane geometry with permitted | |
| maneuvers, intersection topology (controlled/uncontrolled, signals/signs), | |
| crosswalks, and multi-actor right-of-way states + intent. | |
| A bank is a pure function of (n_scenarios, seed, model_version): the same | |
| inputs produce byte-identical scenarios. `model_version` selects a stack | |
| profile that determines — deterministically per scenario — whether the | |
| stack COMMITS each planted violation opportunity; this is how | |
| baseline-vs-candidate regression banks are materialized (the bank is "the | |
| log of that software driving the same scenario set", i.e. resimulation). | |
| Planted truth includes NON-violations that superficially resemble | |
| violations (legal assertive merge; ego yielding while holding right-of-way) | |
| so the rule engine's false-accusation rate is measurable. | |
| All numeric thresholds/kinematics here are ILLUSTRATIVE (synthetic | |
| substrate); the generator's contract, not its constants, is the deliverable. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import math | |
| from typing import Dict, List, Optional, Tuple | |
| import numpy as np | |
| # REUSE (landed, read-only): platform time base and nominal ego speed. | |
| from sensorflow.bevfusion.scenes import DT, EGO_SPEED | |
| from sensorflow.rotr import SOFTWARE_VERSION | |
| from sensorflow.rotr.models import ( | |
| Actor, ActorObservation, ActorState, Crosswalk, EgoState, Environment, | |
| Lane, PlannedPoint, PlantedTruth, Provenance, ROTRScenario, RoadContext, | |
| ) | |
| N_FRAMES = 70 | |
| X0 = -10.0 | |
| V_NOM = float(EGO_SPEED) # 10 m/s | |
| LANE_W = 3.5 | |
| # Intersection geometry shared by intersection templates. | |
| STOP_LINE_X = 38.0 | |
| CW_X_MIN, CW_X_MAX = 39.5, 44.5 # crosswalk span along x | |
| BOX_X_MIN, BOX_X_MAX = 39.5, 52.0 | |
| DATASET_VERSION = "rotr-bank-1" | |
| GENERATOR_VERSION = "rotr-scenes-1.0.0" | |
| # Stack profiles: per-cause commit probability for planted violation | |
| # opportunities. Deterministic per scenario (seeded rng draw). | |
| MODEL_PROFILES: Dict[str, Dict[str, float]] = { | |
| "stack-v1": {"perception": 0.90, "prediction": 0.90, "planning": 0.50, | |
| "localization": 1.00, "map": 1.00, "control": 0.80, | |
| "data_label": 1.00}, | |
| "stack-v2-improved": {"perception": 0.25, "prediction": 0.90, | |
| "planning": 0.50, "localization": 1.00, | |
| "map": 1.00, "control": 0.80, "data_label": 1.00}, | |
| "stack-v3-planning-regression": {"perception": 0.25, "prediction": 0.90, | |
| "planning": 1.00, "localization": 1.00, | |
| "map": 1.00, "control": 0.80, | |
| "data_label": 1.00}, | |
| } | |
| DEFAULT_MODEL = "stack-v1" | |
| # ------------------------------------------------------------ contexts | |
| def _lane(lane_id: str, y: float, maneuvers: List[str], | |
| restricted_to: Optional[str] = None) -> Lane: | |
| return Lane(lane_id=lane_id, center_y=y, permitted_maneuvers=maneuvers, | |
| restricted_to=restricted_to) | |
| def _ctx_uncontrolled() -> RoadContext: | |
| return RoadContext( | |
| context_id="uncontrolled-xing", intersection_type="uncontrolled", | |
| control="none", stop_line_x=STOP_LINE_X, | |
| intersection_x_min=BOX_X_MIN, intersection_x_max=BOX_X_MAX, | |
| lanes=[_lane("L0", 0.0, ["STRAIGHT", "LANE_CHANGE"]), | |
| _lane("L-1", -LANE_W, ["STRAIGHT", "LANE_CHANGE"])], | |
| crosswalks=[Crosswalk(crosswalk_id="cw1", x_min=CW_X_MIN, x_max=CW_X_MAX)]) | |
| def _ctx_bus(restricted: bool) -> RoadContext: | |
| return RoadContext( | |
| context_id="bus-corridor", intersection_type="none", control="none", | |
| lanes=[_lane("L0", 0.0, ["STRAIGHT", "LANE_CHANGE"]), | |
| _lane("LBUS", -LANE_W, ["STRAIGHT"], | |
| restricted_to="bus" if restricted else None)]) | |
| def _ctx_turnlane() -> RoadContext: | |
| return RoadContext( | |
| context_id="turn-lane-xing", intersection_type="uncontrolled", | |
| control="none", stop_line_x=STOP_LINE_X, | |
| intersection_x_min=BOX_X_MIN, intersection_x_max=BOX_X_MAX, | |
| lanes=[_lane("L0", 0.0, ["STRAIGHT", "LANE_CHANGE"]), | |
| _lane("LT", LANE_W, ["LEFT_TURN"])]) | |
| def _ctx_signal(mapped_signal: bool) -> RoadContext: | |
| return RoadContext( | |
| context_id="signal-xing", intersection_type="controlled", | |
| control="signal" if mapped_signal else "none", | |
| signal_state_for_ego="red" if mapped_signal else None, | |
| stop_line_x=STOP_LINE_X, | |
| intersection_x_min=BOX_X_MIN, intersection_x_max=BOX_X_MAX, | |
| lanes=[_lane("L0", 0.0, ["STRAIGHT"])], | |
| crosswalks=[Crosswalk(crosswalk_id="cw1", x_min=CW_X_MIN, x_max=CW_X_MAX)]) | |
| def _ctx_stopsign() -> RoadContext: | |
| return RoadContext( | |
| context_id="stopsign-xing", intersection_type="controlled", | |
| control="stop_sign", stop_line_x=STOP_LINE_X, | |
| intersection_x_min=BOX_X_MIN, intersection_x_max=BOX_X_MAX, | |
| lanes=[_lane("L0", 0.0, ["STRAIGHT"])], | |
| crosswalks=[Crosswalk(crosswalk_id="cw1", x_min=CW_X_MIN, x_max=CW_X_MAX)]) | |
| def _ctx_merge() -> RoadContext: | |
| return RoadContext( | |
| context_id="merge-corridor", intersection_type="none", control="none", | |
| lanes=[_lane("L0", 0.0, ["STRAIGHT", "LANE_CHANGE"]), | |
| _lane("L-1", -LANE_W, ["STRAIGHT", "LANE_CHANGE"])]) | |
| # ------------------------------------------------------------ trajectories | |
| def _traj_const(v: float = V_NOM, y: float = 0.0) -> List[Dict]: | |
| return [{"t": fi * DT, "x": X0 + v * fi * DT, "y": y, "speed": v, | |
| "accel": 0.0} for fi in range(N_FRAMES)] | |
| def _traj_stop(x_stop: float, decel: float = 3.0, v0: float = V_NOM, | |
| y: float = 0.0, resume_t: Optional[float] = None, | |
| resume_accel: float = 2.0) -> List[Dict]: | |
| """Cruise, brake to a stop at x_stop, optionally resume.""" | |
| d_brake = v0 * v0 / (2 * decel) | |
| x_brake = x_stop - d_brake | |
| pts, x, v, a = [], X0, v0, 0.0 | |
| for fi in range(N_FRAMES): | |
| t = fi * DT | |
| pts.append({"t": t, "x": x, "y": y, "speed": v, "accel": a}) | |
| if v > 0 and x >= x_brake: | |
| a = -decel | |
| elif v <= 0 and resume_t is not None and t >= resume_t: | |
| a = resume_accel | |
| elif v <= 0: | |
| a = 0.0 | |
| v = max(0.0, min(v0, v + a * DT)) | |
| if v == 0.0: | |
| a = 0.0 | |
| x += v * DT | |
| return pts | |
| def _traj_rolling(x_line: float, v_min: float = 3.0, v0: float = V_NOM, | |
| y: float = 0.0) -> List[Dict]: | |
| """Slow to v_min at the stop line, never stop, resume to v0.""" | |
| decel = 2.5 | |
| d = (v0 * v0 - v_min * v_min) / (2 * decel) | |
| x_brake = x_line - d | |
| pts, x, v, a = [], X0, v0, 0.0 | |
| for fi in range(N_FRAMES): | |
| t = fi * DT | |
| pts.append({"t": t, "x": x, "y": y, "speed": v, "accel": a}) | |
| if x >= x_brake and v > v_min and x < x_line: | |
| a = -decel | |
| elif x >= x_line and v < v0: | |
| a = 2.0 | |
| else: | |
| a = 0.0 | |
| v = max(v_min if x >= x_brake else 0.0, min(v0, v + a * DT)) | |
| x += v * DT | |
| return pts | |
| def _traj_lane_change(t_start: float, t_end: float, y_from: float, | |
| y_to: float, v: float = V_NOM) -> List[Dict]: | |
| pts = [] | |
| for fi in range(N_FRAMES): | |
| t = fi * DT | |
| if t <= t_start: | |
| y = y_from | |
| elif t >= t_end: | |
| y = y_to | |
| else: | |
| frac = (t - t_start) / (t_end - t_start) | |
| y = y_from + (y_to - y_from) * (1 - math.cos(math.pi * frac)) / 2 | |
| pts.append({"t": t, "x": X0 + v * t, "y": y, "speed": v, "accel": 0.0}) | |
| return pts | |
| def _ego_states(pts: List[Dict], context: RoadContext, | |
| loc_offset_y: float = 0.0) -> List[EgoState]: | |
| """Attach true lane association + localization (believed) view.""" | |
| out = [] | |
| for p in pts: | |
| lane = _nearest_lane(context, p["y"]) | |
| out.append(EgoState( | |
| t=p["t"], x=p["x"], y=p["y"], yaw=0.0, speed=p["speed"], | |
| accel=p["accel"], lane_id=lane.lane_id if lane else None, | |
| believed_x=p["x"], believed_y=p["y"] + loc_offset_y, | |
| believed_lane_id=(_nearest_lane(context, p["y"] + loc_offset_y).lane_id | |
| if _nearest_lane(context, p["y"] + loc_offset_y) | |
| else None))) | |
| return out | |
| def _nearest_lane(context: RoadContext, y: float) -> Optional[Lane]: | |
| best, best_d = None, 1e9 | |
| for lane in context.lanes: | |
| d = abs(lane.center_y - y) | |
| if d < best_d: | |
| best, best_d = lane, d | |
| return best if best_d <= LANE_W * 0.6 else None | |
| def _planned(pts: List[Dict]) -> List[PlannedPoint]: | |
| return [PlannedPoint(t=p["t"], x=p["x"], y=p["y"], v=p["speed"]) | |
| for p in pts] | |
| # ------------------------------------------------------------ actors | |
| def _observe(rng: np.random.Generator, st: Dict, cls: str, | |
| mode: str) -> Optional[ActorObservation]: | |
| """Plant the stack's observation of a GT actor state. | |
| mode: good | missed | late(handled by caller) | absent(None=telemetry gap) | |
| """ | |
| if mode == "absent": | |
| return None | |
| if mode == "missed": | |
| return ActorObservation(detected=False) | |
| return ActorObservation( | |
| detected=True, | |
| x=st["x"] + float(rng.normal(0, 0.12)), | |
| y=st["y"] + float(rng.normal(0, 0.08)), | |
| class_name=cls) | |
| def _actor(rng: np.random.Generator, actor_id: str, cls: str, | |
| dims: List[float], row: str, intent: str, | |
| predicted_intent: Optional[str], | |
| state_fn, obs_mode_fn) -> Actor: | |
| states = [] | |
| for fi in range(N_FRAMES): | |
| t = fi * DT | |
| x, y, vx, vy, yaw = state_fn(t) | |
| st = {"x": x, "y": y} | |
| states.append(ActorState( | |
| t=t, x=x, y=y, vx=vx, vy=vy, yaw=yaw, | |
| observation=_observe(rng, st, cls, obs_mode_fn(t)))) | |
| return Actor(actor_id=actor_id, class_name=cls, dims=dims, | |
| right_of_way=row, intent=intent, | |
| predicted_intent=predicted_intent, states=states) | |
| def _crossing_ped_fn(x: float = 44.9, y0: float = 6.0, vy: float = -1.4, | |
| y_end: float = -7.0): | |
| # x=44.9 places the crossing at the far edge of the crosswalk so a | |
| # non-yielding ego produces a deterministic NEAR-miss (sub-meter | |
| # clearance), not a synthetic collision. | |
| def fn(t: float) -> Tuple[float, float, float, float, float]: | |
| y = max(y_end, y0 + vy * t) | |
| moving = y > y_end | |
| return x, y, 0.0, vy if moving else 0.0, -math.pi / 2 | |
| return fn | |
| def _waiting_ped_fn(x: float = 42.5, y: float = 5.5): | |
| def fn(t: float): | |
| return x, y, 0.0, 0.0, -math.pi / 2 | |
| return fn | |
| def _crossing_vehicle_fn(x: float = 45.5, y0: float = 20.0, vy: float = -2.8): | |
| def fn(t: float): | |
| return x, y0 + vy * t, 0.0, vy, -math.pi / 2 | |
| return fn | |
| def _trailing_vehicle_fn(x0: float, vx: float = 13.0, | |
| brake_t: Optional[float] = None, | |
| brake_a: float = 2.5, v_min: float = 8.5): | |
| def fn(t: float): | |
| if brake_t is None or t <= brake_t: | |
| return x0 + vx * t, -LANE_W, vx, 0.0, 0.0 | |
| tb = t - brake_t | |
| v = max(v_min, vx - brake_a * tb) | |
| t_ramp = (vx - v_min) / brake_a | |
| if tb <= t_ramp: | |
| x = x0 + vx * brake_t + vx * tb - 0.5 * brake_a * tb * tb | |
| else: | |
| x = (x0 + vx * brake_t + vx * t_ramp - 0.5 * brake_a * t_ramp ** 2 | |
| + v_min * (tb - t_ramp)) | |
| return x, -LANE_W, v, 0.0, 0.0 | |
| return fn | |
| ALWAYS_GOOD = lambda t: "good" # noqa: E731 | |
| NEVER_SEEN = lambda t: "missed" # noqa: E731 | |
| TELEMETRY_GAP = lambda t: "absent" # noqa: E731 | |
| # ------------------------------------------------------------ templates | |
| # | |
| # Each template returns the scenario pieces given (rng, committed). | |
| # `cause` is the planted causal layer for the committed variant. | |
| VEH_DIMS = [4.5, 1.9, 1.6] | |
| PED_DIMS = [0.6, 0.6, 1.75] | |
| def _tpl_fail_yield_ped(rng, committed: bool, cause: str) -> Dict: | |
| ctx = _ctx_uncontrolled() | |
| obs_mode = NEVER_SEEN if (committed and cause == "perception") else \ | |
| (TELEMETRY_GAP if (committed and cause == "data_label") else ALWAYS_GOOD) | |
| predicted = None if cause == "data_label" else \ | |
| ("WAITING" if (committed and cause == "prediction") else "CROSSING") | |
| ped = _actor(rng, "ped-1", "pedestrian", PED_DIMS, "HAS_ROW", "CROSSING", | |
| predicted, _crossing_ped_fn(), obs_mode) | |
| if committed: | |
| exec_pts = _traj_const() | |
| plan_pts = exec_pts | |
| else: | |
| exec_pts = _traj_stop(STOP_LINE_X - 0.5, resume_t=6.5) | |
| plan_pts = exec_pts | |
| ego = _ego_states(exec_pts, ctx) | |
| if committed and cause == "data_label": | |
| # Planted record-integrity fault: disordered timestamps mid-approach. | |
| for k in range(30, 34): | |
| ego[k].t = ego[k].t - 0.35 | |
| return {"map_context": ctx, "actual_context": ctx, "ego": ego, | |
| "planned": _planned(plan_pts), "actors": [ped], | |
| "expected_rule": "R-YIELD-PED-01", | |
| "description": "pedestrian crossing at uncontrolled intersection; " | |
| + ("ego proceeds without yielding" if committed | |
| else "ego yields correctly")} | |
| def _tpl_restricted_path(rng, committed: bool, cause: str) -> Dict: | |
| actual = _ctx_bus(restricted=True) | |
| mapped = _ctx_bus(restricted=(cause != "map")) | |
| if committed: | |
| pts = _traj_lane_change(2.0, 3.5, 0.0, -LANE_W) | |
| else: | |
| pts = _traj_const() | |
| return {"map_context": mapped, "actual_context": actual, | |
| "ego": _ego_states(pts, actual), "planned": _planned(pts), | |
| "actors": [], | |
| "expected_rule": "R-PATH-RESTRICT-02", | |
| "description": "bus-restricted lane; " | |
| + ("ego enters restricted path" if committed | |
| else "ego stays in general lane") | |
| + (" (restriction missing from map)" | |
| if cause == "map" else "")} | |
| def _tpl_wrong_lane(rng, committed: bool, cause: str) -> Dict: | |
| ctx = _ctx_turnlane() | |
| # Ego actually drives the left-turn-only lane straight through; its | |
| # localization believes it is in the through lane (offset -LANE_W). | |
| pts = _traj_const(y=LANE_W) | |
| ego = _ego_states(pts, ctx, loc_offset_y=-LANE_W) | |
| return {"map_context": ctx, "actual_context": ctx, "ego": ego, | |
| "planned": _planned(pts), "actors": [], | |
| "expected_rule": "R-LANE-MANEUVER-03", | |
| "description": "ego proceeds straight from a left-turn-only lane " | |
| "(wrong lane association from localization error)"} | |
| def _tpl_signal_conflict(rng, committed: bool, cause: str) -> Dict: | |
| actual = _ctx_signal(mapped_signal=True) | |
| mapped = _ctx_signal(mapped_signal=(cause != "map")) | |
| obs_mode = NEVER_SEEN if (committed and cause == "perception") else ALWAYS_GOOD | |
| veh = _actor(rng, "xveh-1", "vehicle", VEH_DIMS, "HAS_ROW", "CROSSING", | |
| "CROSSING", _crossing_vehicle_fn(), obs_mode) | |
| if committed: | |
| pts = _traj_const() | |
| else: | |
| pts = _traj_stop(STOP_LINE_X - 0.5) | |
| return {"map_context": mapped, "actual_context": actual, | |
| "ego": _ego_states(pts, actual), "planned": _planned(pts), | |
| "actors": [veh], | |
| "expected_rule": "R-INT-CONFLICT-04", | |
| "description": "red signal with conflicting cross traffic; " | |
| + ("ego enters on red" if committed | |
| else "ego stops at the line") | |
| + (" (signal unmapped)" if cause == "map" else "")} | |
| def _tpl_unsafe_merge(rng, committed: bool, cause: str) -> Dict: | |
| ctx = _ctx_merge() | |
| if committed: | |
| veh = _actor(rng, "mveh-1", "vehicle", VEH_DIMS, "HAS_ROW", | |
| "PROCEEDING", "PROCEEDING", | |
| _trailing_vehicle_fn(-31.5, brake_t=4.6), ALWAYS_GOOD) | |
| pts = _traj_lane_change(3.0, 4.5, 0.0, -LANE_W) | |
| else: | |
| veh = _actor(rng, "mveh-1", "vehicle", VEH_DIMS, "HAS_ROW", | |
| "PROCEEDING", "PROCEEDING", | |
| _trailing_vehicle_fn(-58.0), ALWAYS_GOOD) | |
| pts = _traj_const() | |
| return {"map_context": ctx, "actual_context": ctx, | |
| "ego": _ego_states(pts, ctx), "planned": _planned(pts), | |
| "actors": [veh], | |
| "expected_rule": "R-MERGE-GAP-05", | |
| "description": "merge ahead of right-of-way traffic with " | |
| + ("insufficient gap" if committed else "safe gap")} | |
| def _tpl_stop_overshoot(rng, committed: bool, cause: str) -> Dict: | |
| ctx = _ctx_stopsign() | |
| ped = _actor(rng, "ped-w1", "pedestrian", PED_DIMS, "NONE", "WAITING", | |
| "WAITING", _waiting_ped_fn(), ALWAYS_GOOD) | |
| plan_pts = _traj_stop(STOP_LINE_X - 0.5, resume_t=5.6) | |
| if committed: | |
| exec_pts = _traj_rolling(STOP_LINE_X) # actuation under-brakes | |
| else: | |
| exec_pts = plan_pts | |
| return {"map_context": ctx, "actual_context": ctx, | |
| "ego": _ego_states(exec_pts, ctx), "planned": _planned(plan_pts), | |
| "actors": [ped], | |
| "expected_rule": "R-STOP-06", | |
| "description": "stop sign; " + ("planned stop, executed roll-through " | |
| "(control tracking error)" | |
| if committed else "full stop made")} | |
| def _tpl_legal_merge(rng, committed: bool, cause: str) -> Dict: | |
| d = _tpl_unsafe_merge(rng, committed=False, cause="") | |
| # Assertive but legal: the merge happens, with a sufficient gap. | |
| ctx = d["actual_context"] | |
| pts = _traj_lane_change(3.0, 4.5, 0.0, -LANE_W) | |
| d.update({"ego": _ego_states(pts, ctx), "planned": _planned(pts), | |
| "expected_rule": None, | |
| "description": "legal assertive merge (sufficient gap; planted " | |
| "NON-violation that superficially resembles one)"}) | |
| return d | |
| def _tpl_yield_with_row(rng, committed: bool, cause: str) -> Dict: | |
| ctx = _ctx_uncontrolled() | |
| ped = _actor(rng, "ped-c1", "pedestrian", PED_DIMS, "NONE", "WAITING", | |
| "WAITING", _waiting_ped_fn(), ALWAYS_GOOD) | |
| pts = _traj_stop(STOP_LINE_X - 0.5, resume_t=5.8) | |
| return {"map_context": ctx, "actual_context": ctx, | |
| "ego": _ego_states(pts, ctx), "planned": _planned(pts), | |
| "actors": [ped], | |
| "expected_rule": None, | |
| "description": "ego yields although it holds right-of-way (curb " | |
| "pedestrian is waiting; planted NON-violation)"} | |
| def _tpl_green_proceed(rng, committed: bool, cause: str) -> Dict: | |
| ctx = _ctx_signal(mapped_signal=True) | |
| ctx.signal_state_for_ego = "green" | |
| pts = _traj_const() | |
| return {"map_context": ctx, "actual_context": ctx.model_copy(deep=True), | |
| "ego": _ego_states(pts, ctx), "planned": _planned(pts), | |
| "actors": [], | |
| "expected_rule": None, | |
| "description": "normal green-light transit (planted NON-violation)"} | |
| # (template fn, planted kind, cause layer or None for non-violations) | |
| TEMPLATES = [ | |
| (_tpl_fail_yield_ped, "fail_yield_pedestrian", "perception"), | |
| (_tpl_fail_yield_ped, "fail_yield_pedestrian", "planning"), | |
| (_tpl_fail_yield_ped, "fail_yield_pedestrian", "prediction"), | |
| (_tpl_restricted_path, "restricted_path_entry", "planning"), | |
| (_tpl_restricted_path, "restricted_path_entry", "map"), | |
| (_tpl_wrong_lane, "wrong_lane_association", "localization"), | |
| (_tpl_signal_conflict, "intersection_conflict", "map"), | |
| (_tpl_signal_conflict, "intersection_conflict", "perception"), | |
| (_tpl_unsafe_merge, "unsafe_merge", "planning"), | |
| (_tpl_stop_overshoot, "stop_overshoot", "control"), | |
| (_tpl_fail_yield_ped, "fail_yield_pedestrian", "data_label"), | |
| (_tpl_legal_merge, "legal_assertive_merge", None), | |
| (_tpl_yield_with_row, "yield_with_right_of_way", None), | |
| (_tpl_green_proceed, "green_proceed", None), | |
| ] | |
| VISIBILITIES = ["low", "clear", "clear"] | |
| LIGHTINGS = ["day", "dusk", "night"] | |
| WEATHERS = ["clear", "clear", "rain"] | |
| def generate_bank(n_scenarios: int = 28, seed: int = 7, | |
| model_version: str = DEFAULT_MODEL) -> List[ROTRScenario]: | |
| """Deterministic scenario bank; same inputs -> identical output.""" | |
| if model_version not in MODEL_PROFILES: | |
| raise ValueError(f"unknown model_version {model_version!r}; known: " | |
| f"{sorted(MODEL_PROFILES)}") | |
| profile = MODEL_PROFILES[model_version] | |
| bank_id = bank_id_for(n_scenarios, seed, model_version) | |
| out: List[ROTRScenario] = [] | |
| for i in range(n_scenarios): | |
| # Same per-sequence seeding discipline as bevfusion.scenes. | |
| rng = np.random.default_rng(seed * 100003 + i * 7919) | |
| tpl_fn, kind, cause = TEMPLATES[i % len(TEMPLATES)] | |
| is_opportunity = cause is not None | |
| committed = bool(is_opportunity | |
| and rng.random() < profile.get(cause, 1.0)) | |
| parts = tpl_fn(rng, committed, cause or "") | |
| env = Environment(visibility=VISIBILITIES[i % 3], | |
| lighting=LIGHTINGS[i % 3], | |
| weather=WEATHERS[i % 3]) | |
| sid = f"{bank_id}-sc{i:03d}" | |
| out.append(ROTRScenario( | |
| scenario_id=sid, bank_id=bank_id, seed=seed, | |
| description=parts["description"], environment=env, | |
| map_context=parts["map_context"], | |
| actual_context=parts["actual_context"], | |
| ego=parts["ego"], planned=parts["planned"], | |
| actors=parts["actors"], | |
| planted=PlantedTruth( | |
| kind=kind, is_violation_opportunity=is_opportunity, | |
| committed=committed, | |
| expected_rule_id=parts["expected_rule"] if committed else None, | |
| cause_layer=cause if committed else None, | |
| notes=parts["description"]), | |
| provenance=Provenance( | |
| scenario_id=sid, dataset_version=DATASET_VERSION, | |
| model_version=model_version, | |
| software_version=f"{SOFTWARE_VERSION}/{GENERATOR_VERSION}", | |
| source="SYNTHETIC", confidence=1.0))) | |
| return out | |
| def bank_id_for(n_scenarios: int, seed: int, model_version: str) -> str: | |
| blob = f"{n_scenarios}|{seed}|{model_version}|{GENERATOR_VERSION}" | |
| return "bank-" + hashlib.sha256(blob.encode()).hexdigest()[:10] | |