File size: 11,221 Bytes
f234112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
"""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