ATA / src /integration /emergency.py
vishanth11
Deploy AI Traffic Adaptation System with Git LFS binaries to Hugging Face Spaces
f234112
Raw History Blame Contribute Delete
11.2 kB
"""Topology-adaptive emergency priority planning for the SUMO controller."""
from __future__ import annotations
import math
from dataclasses import dataclass
EMERGENCY_TYPE_NAMES = {
"ambulance",
"emergency",
"emergency_vehicle",
"fire",
"firetruck",
"police",
}
# SUMO TraCI vehicle-signal bits.
# Bit 3 = rear brake-light dots; bit 11 = blue emergency light.
EMERGENCY_REAR_MARKER_SIGNAL = 1 << 3
EMERGENCY_BLUE_SIGNAL = 1 << 11
EMERGENCY_VISUAL_SIGNALS = EMERGENCY_REAR_MARKER_SIGNAL | EMERGENCY_BLUE_SIGNAL
@dataclass(frozen=True)
class EmergencyVehicleState:
vehicle_id: str
vehicle_type: str
current_edge: str | None
current_lane: str | None
current_position: float | None
distance_to_junction: float | None
speed_mps: float | None
route: tuple[str, ...]
current_junction: str | None
next_junction: str | None
destination: str | None
emergency: bool
confidence: float
eta_seconds: float | None
@dataclass(frozen=True)
class EmergencyPlan:
vehicle_id: str
junction_id: str
required_movement: str | None
candidate_phases: tuple[str, ...]
selected_phase: str | None
eta_seconds: float | None
urgency: float
safe: bool
current_edge: str | None
next_edge: str | None
distance_to_junction: float | None
vehicle_speed_mps: float | None
conflicting_movements: tuple[str, ...]
class EmergencyCorridorManager:
"""Create emergency phase plans without directly controlling signals."""
def __init__(self, traci_connection, max_speed_floor=0.5):
self.traci = traci_connection
self.max_speed_floor = float(max_speed_floor)
self.active_vehicle_ids = set()
self.released_vehicle_ids = set()
@staticmethod
def _safe_call(function, *args, default=None):
try:
return function(*args)
except Exception:
return default
@classmethod
def _is_emergency(cls, vehicle_type, vehicle_class):
values = {
str(vehicle_type or "").lower(),
str(vehicle_class or "").lower(),
}
return bool(values.intersection(EMERGENCY_TYPE_NAMES)) or any(
"emergency" in value or "ambulance" in value or "fire" in value
for value in values
)
def _apply_visual_markers(self, vehicle_ids):
"""Apply only the requested emergency visualization signals.
Emergency vehicles get two rear red light dots and the blue emergency
light. Normal vehicles get signal state 0 so they remain visually plain.
No left/right blinkers are enabled here.
"""
set_signals = getattr(self.traci.vehicle, "setSignals", None)
if set_signals is None:
return
for vehicle_id in vehicle_ids:
vehicle_type = self._safe_call(
self.traci.vehicle.getTypeID, vehicle_id, default=""
)
vehicle_class = self._safe_call(
self.traci.vehicle.getVehicleClass, vehicle_id, default=""
)
is_emergency = self._is_emergency(vehicle_type, vehicle_class)
signal_state = EMERGENCY_VISUAL_SIGNALS if is_emergency else 0
self._safe_call(set_signals, vehicle_id, signal_state, default=None)
def _junction_for_movement(self, movement, topologies):
for junction_id, topology in topologies.items():
if movement in topology.movements:
return junction_id
return None
def discover(self, topologies):
"""Discover current emergency vehicles from SUMO type/class data."""
states = []
current_ids = set(self.traci.vehicle.getIDList())
# Apply the visual state every simulation step.
self._apply_visual_markers(current_ids)
for vehicle_id in current_ids:
vehicle_type = self._safe_call(
self.traci.vehicle.getTypeID, vehicle_id, default=""
)
vehicle_class = self._safe_call(
self.traci.vehicle.getVehicleClass, vehicle_id, default=""
)
if not self._is_emergency(vehicle_type, vehicle_class):
continue
current_edge = self._safe_call(
self.traci.vehicle.getRoadID, vehicle_id, default=None
)
if current_edge and str(current_edge).startswith(":"):
current_edge = None
lane_id = self._safe_call(
self.traci.vehicle.getLaneID, vehicle_id, default=None
)
position = self._safe_call(
self.traci.vehicle.getLanePosition, vehicle_id, default=None
)
lane_length = (
self._safe_call(self.traci.lane.getLength, lane_id, default=None)
if lane_id else None
)
speed = self._safe_call(
self.traci.vehicle.getSpeed, vehicle_id, default=None
)
speed = float(speed) if speed is not None else None
route = tuple(
self._safe_call(self.traci.vehicle.getRoute, vehicle_id, default=[])
or []
)
route_index = self._safe_call(
self.traci.vehicle.getRouteIndex, vehicle_id, default=None
)
next_edge = None
if route_index is not None and route_index + 1 < len(route):
next_edge = route[route_index + 1]
if next_edge and str(next_edge).startswith(":"):
next_edge = None
current_junction = None
required_movement = None
for junction_id, topology in topologies.items():
for movement_id, movement in topology.movements.items():
if movement.from_road == current_edge and movement.to_road == next_edge:
current_junction = junction_id
required_movement = movement_id
break
if current_junction:
break
distance = None
if lane_length is not None and position is not None:
distance = max(0.0, float(lane_length) - float(position))
eta = None
if distance is not None and speed is not None and speed >= self.max_speed_floor:
eta = distance / speed
next_junction = None
if required_movement:
movement = topologies[current_junction].movements[required_movement]
for junction_id, topology in topologies.items():
if any(
candidate.from_road == movement.to_road
for candidate in topology.movements.values()
):
next_junction = junction_id
break
states.append(
EmergencyVehicleState(
vehicle_id=str(vehicle_id),
vehicle_type=str(vehicle_type),
current_edge=current_edge,
current_lane=lane_id,
current_position=float(position) if position is not None else None,
distance_to_junction=distance,
speed_mps=speed,
route=route,
current_junction=current_junction,
next_junction=next_junction,
destination=route[-1] if route else None,
emergency=True,
confidence=1.0,
eta_seconds=eta,
)
)
return states
@staticmethod
def urgency(state):
"""Normalize urgency from ETA, with safe handling for missing/zero speed."""
if state.eta_seconds is None:
return 0.35
if state.eta_seconds <= 0.0:
return 1.0
return max(0.0, min(1.0, math.exp(-state.eta_seconds / 30.0)))
def plan_for_junction(self, state, junction_id, topology, phases, generator):
"""Build a safe candidate plan for the vehicle's required movement."""
movement_id = None
for candidate_id, movement in topology.movements.items():
if movement.from_road == state.current_edge:
next_edge = (
state.route[state.route.index(state.current_edge) + 1]
if state.current_edge in state.route
and state.route.index(state.current_edge) + 1 < len(state.route)
else None
)
if movement.to_road == next_edge:
movement_id = candidate_id
break
if movement_id is None:
return EmergencyPlan(
state.vehicle_id, junction_id, None, (), None,
state.eta_seconds, self.urgency(state), False,
state.current_edge, None, state.distance_to_junction,
state.speed_mps, ()
)
candidate_phases = tuple(
phase["phase_id"]
for phase in phases
if movement_id in phase["movements"]
and generator.is_compatible(phase["movements"])
)
selected = candidate_phases[0] if candidate_phases else None
conflicting = tuple(
movement_id_2
for movement_id_2 in topology.movements
if movement_id_2 != movement_id
and generator.graph.are_conflicting(movement_id, movement_id_2)
)
return EmergencyPlan(
state.vehicle_id,
junction_id,
movement_id,
candidate_phases,
selected,
state.eta_seconds,
self.urgency(state),
bool(selected),
state.current_edge,
topology.movements[movement_id].to_road,
state.distance_to_junction,
state.speed_mps,
conflicting,
)
def plans(self, states, topologies, runtimes):
"""Return the most urgent valid plan for each junction."""
result = {}
for junction_id, runtime in runtimes.items():
candidates = [
state for state in states
if state.current_junction == junction_id
]
plans = [
self.plan_for_junction(
state,
junction_id,
runtime["topology"],
runtime["phases"],
runtime["generator"],
)
for state in candidates
]
plans = [plan for plan in plans if plan.required_movement]
if plans:
result[junction_id] = max(
plans,
key=lambda plan: (plan.urgency, -(plan.eta_seconds or 1e9)),
)
return result
def release_completed(self, states):
current = {state.vehicle_id for state in states}
released = self.active_vehicle_ids - current
self.released_vehicle_ids.update(released)
self.active_vehicle_ids = current
return released