ATA / src /integration /sumo_phase_adapter.py
vishanth11
fix: Add traci/sumolib to requirements and safe fallback imports
eb1ca88
Raw History Blame Contribute Delete
11.2 kB
try:
import traci
except ImportError:
traci = None
class SUMOPhaseAdapter:
"""
Converts SUMO traffic-light phases into a generic
representation that can be used by the decision engine.
SUMO-specific link indices are kept here instead of
being placed inside the generic topology classes.
"""
def __init__(self, traci_connection=None):
self.traci = traci_connection or traci
# ============================================================
# GET ALL SIGNAL PROGRAMS
# ============================================================
def get_programs(self, junction_id):
"""
Return all signal programs available at a junction.
"""
programs = (
self.traci.trafficlight.getAllProgramLogics(
junction_id
)
)
result = []
for program in programs:
program_data = {
"program_id": program.programID,
"type": program.type,
"current_phase": (
self.traci.trafficlight.getPhase(
junction_id
)
),
"phases": []
}
for phase_index, phase in enumerate(
program.phases
):
program_data["phases"].append({
"phase_index": phase_index,
"duration": phase.duration,
"min_duration": phase.minDur,
"max_duration": phase.maxDur,
"state": phase.state
})
result.append(program_data)
return result
# ============================================================
# GET CURRENT PROGRAM
# ============================================================
def get_current_program(self, junction_id):
"""
Get the currently active SUMO signal program.
"""
program_id = (
self.traci.trafficlight.getProgram(
junction_id
)
)
programs = self.get_programs(junction_id)
for program in programs:
if program["program_id"] == program_id:
return program
return None
# ============================================================
# GET CURRENT PHASE
# ============================================================
def get_current_phase(self, junction_id):
"""
Return the current phase index.
"""
return self.traci.trafficlight.getPhase(
junction_id
)
# ============================================================
# GET PHASE STATE
# ============================================================
def get_phase_state(
self,
junction_id,
phase_index
):
"""
Return the signal state string for a phase.
Example:
GGggrrrrGGggrrrr
"""
program = self.get_current_program(
junction_id
)
if program is None:
return None
for phase in program["phases"]:
if phase["phase_index"] == phase_index:
return phase["state"]
return None
# ============================================================
# GET GREEN LINKS
# ============================================================
def get_green_links(
self,
junction_id,
phase_index
):
"""
Return link indices that are green during
the specified phase.
SUMO signal characters:
G = green
g = permissive green
y = yellow
r = red
Both G and g are considered active green.
"""
state = self.get_phase_state(
junction_id,
phase_index
)
if state is None:
return []
green_links = []
for link_index, signal in enumerate(state):
if signal in ("G", "g"):
green_links.append(
link_index
)
return green_links
# ============================================================
# GET PHASE TYPE
# ============================================================
def get_phase_type(
self,
junction_id,
phase_index
):
"""
Classify a phase as:
green
yellow
red
mixed
"""
state = self.get_phase_state(
junction_id,
phase_index
)
if state is None:
return "unknown"
has_green = any(
signal in ("G", "g")
for signal in state
)
has_yellow = any(
signal == "y"
for signal in state
)
has_red = any(
signal == "r"
for signal in state
)
if has_green:
return "green"
if has_yellow:
return "yellow"
if has_red:
return "red"
return "mixed"
# ============================================================
# GET DECISION PHASES
# ============================================================
def get_decision_phases(self, junction_id):
"""
Return phases that can be considered normal
green decision phases.
Yellow phases are deliberately excluded.
"""
program = self.get_current_program(
junction_id
)
if program is None:
return []
decision_phases = []
for phase in program["phases"]:
phase_index = phase["phase_index"]
phase_type = self.get_phase_type(
junction_id,
phase_index
)
if phase_type == "green":
decision_phases.append({
"phase_index": phase_index,
"duration": phase["duration"],
"state": phase["state"],
"green_links": (
self.get_green_links(
junction_id,
phase_index
)
)
})
return decision_phases
# ============================================================
# CONVERT PHASE TO MOVEMENTS
# ============================================================
def phase_to_movements(
self,
junction_id,
phase_index,
movement_metadata
):
"""
Convert SUMO green link indices into movement IDs.
movement_metadata should be the metadata produced
by SUMOTopologyAdapter.
Example:
link 0
↓
A0_M0
link 1
↓
A0_M1
"""
green_links = self.get_green_links(
junction_id,
phase_index
)
movements = []
for movement_id, metadata in (
movement_metadata.items()
):
if (
metadata["junction_id"] == junction_id
and metadata["link_index"]
in green_links
):
movements.append(
movement_id
)
return movements
# ============================================================
# BUILD PHASE REPRESENTATION
# ============================================================
def build_phase_representation(
self,
junction_id,
movement_metadata
):
"""
Build a generic representation of all usable
signal phases at a junction.
"""
decision_phases = (
self.get_decision_phases(
junction_id
)
)
phases = []
for phase in decision_phases:
phase_index = phase[
"phase_index"
]
movement_ids = (
self.phase_to_movements(
junction_id,
phase_index,
movement_metadata
)
)
phases.append({
"phase_id": (
f"{junction_id}_P"
f"{phase_index}"
),
"junction_id": junction_id,
"phase_index": phase_index,
"duration": phase["duration"],
"state": phase["state"],
"green_links": phase[
"green_links"
],
"movements": movement_ids
})
return phases
# ============================================================
# PRINT DEBUG INFORMATION
# ============================================================
def print_junction_phases(
self,
junction_id,
movement_metadata
):
"""
Print a readable representation for debugging.
"""
print()
print("=" * 70)
print(
f"PHASES FOR JUNCTION: {junction_id}"
)
print("=" * 70)
phases = (
self.build_phase_representation(
junction_id,
movement_metadata
)
)
for phase in phases:
print()
print(
f"Phase: {phase['phase_id']}"
)
print(
f" SUMO index: "
f"{phase['phase_index']}"
)
print(
f" Duration: "
f"{phase['duration']} seconds"
)
print(
f" State: "
f"{phase['state']}"
)
print(
f" Green links: "
f"{phase['green_links']}"
)
print(
f" Movements: "
f"{phase['movements']}"
)
# ================================================================
# TEST
# ================================================================
if __name__ == "__main__":
SUMO_CMD = [
"sumo",
"-c",
"simulation/configs/corridor.sumocfg"
]
try:
traci.start(SUMO_CMD)
from integration.sumo_topology_adapter import (
SUMOTopologyAdapter
)
topology_adapter = (
SUMOTopologyAdapter()
)
phase_adapter = (
SUMOPhaseAdapter()
)
# Discover topology first.
topologies = (
topology_adapter
.discover_all_junctions()
)
# Display phases for the main
# corridor junctions.
for junction_id in [
"A0",
"B0",
"C0"
]:
phase_adapter.print_junction_phases(
junction_id,
topology_adapter.sumo_metadata
)
finally:
traci.close()