ATA / src /integration /sumo_demand.py
vishanth11
fix: Add traci/sumolib to requirements and safe fallback imports
eb1ca88
Raw History Blame Contribute Delete
14.5 kB
try:
import traci
except ImportError:
traci = None
class SUMODemandCalculator:
"""
Calculates real-time traffic demand for movements and phases
directly from the running SUMO simulation.
The calculator uses the SUMO vehicle's current lane and route
to determine which junction movement the vehicle belongs to.
Movement matching priority:
1. Exact (from_lane, next_route_edge) match
2. Unique (from_edge, to_edge) match
3. Otherwise ignore the vehicle as ambiguous
This avoids assigning vehicles to movements when their route
information is insufficient to determine the correct movement.
"""
# ============================================================
# MOVEMENT DEMAND
# ============================================================
def calculate_movement_demand(
self,
topology,
movement_metadata=None
):
"""
Calculate current vehicle demand for every allowed movement.
Parameters
----------
topology:
JunctionTopology containing movement definitions.
movement_metadata:
Optional metadata generated by SUMO topology adapter.
Expected format:
{
"A0_M0": {
"junction_id": "A0",
"link_index": 0,
"from_lane": "...",
"to_lane": "...",
"via_lane": "..."
},
...
}
Returns
-------
dict
Example:
{
"A0_M0": 3,
"A0_M1": 5,
"A0_M2": 0,
...
}
"""
# --------------------------------------------------------
# Initialize every allowed movement with zero demand.
# --------------------------------------------------------
demand = {}
allowed_movements = (
topology.get_allowed_movements()
)
for movement in allowed_movements:
demand[movement.movement_id] = 0
# --------------------------------------------------------
# Build lookup structures.
# --------------------------------------------------------
lane_lookup = {}
edge_lookup = {}
for movement in allowed_movements:
movement_id = (
movement.movement_id
)
metadata = None
if movement_metadata is not None:
metadata = movement_metadata.get(
movement_id
)
# ----------------------------------------------------
# Lane-based lookup.
# ----------------------------------------------------
if metadata is not None:
from_lane = metadata.get(
"from_lane"
)
if from_lane:
key = (
from_lane,
)
if key not in lane_lookup:
lane_lookup[key] = []
lane_lookup[key].append(
movement_id
)
# ----------------------------------------------------
# Edge-based fallback lookup.
# ----------------------------------------------------
from_edge = (
movement.from_road
)
to_edge = (
movement.to_road
)
key = (
from_edge,
to_edge
)
if key not in edge_lookup:
edge_lookup[key] = []
edge_lookup[key].append(
movement_id
)
# --------------------------------------------------------
# Examine active vehicles.
# --------------------------------------------------------
vehicle_ids = (
traci.vehicle.getIDList()
)
for vehicle_id in vehicle_ids:
try:
lane_id = (
traci.vehicle.getLaneID(
vehicle_id
)
)
if not lane_id:
continue
# ------------------------------------------------
# Ignore internal SUMO junction lanes.
# ------------------------------------------------
from_edge = (
traci.lane.getEdgeID(
lane_id
)
)
if not from_edge:
continue
if from_edge.startswith(":"):
continue
# ------------------------------------------------
# Get the vehicle's route.
# ------------------------------------------------
route = (
traci.vehicle.getRoute(
vehicle_id
)
)
if not route:
continue
route_index = (
traci.vehicle.getRouteIndex(
vehicle_id
)
)
# ------------------------------------------------
# Determine the next edge.
# ------------------------------------------------
next_edge = None
if (
route_index is not None
and
route_index + 1 < len(route)
):
next_edge = (
route[
route_index + 1
]
)
# ------------------------------------------------
# Ignore internal next edges.
# ------------------------------------------------
if (
next_edge is not None
and
next_edge.startswith(":")
):
next_edge = None
matched_movement = None
# =================================================
# METHOD 1
# Exact lane + next-edge matching.
# =================================================
if next_edge is not None:
lane_candidates = []
for movement in allowed_movements:
movement_id = (
movement.movement_id
)
metadata = None
if movement_metadata is not None:
metadata = (
movement_metadata.get(
movement_id
)
)
if metadata is None:
continue
movement_from_lane = (
metadata.get(
"from_lane"
)
)
movement_to_lane = (
metadata.get(
"to_lane"
)
)
if (
movement_from_lane
== lane_id
):
# Determine the edge reached
# by this movement.
movement_to_edge = (
movement.to_road
)
if (
movement_to_edge
== next_edge
):
lane_candidates.append(
movement_id
)
# Some SUMO networks expose
# a via/internal lane. In that
# case the to-lane may provide
# additional information but
# the destination edge remains
# the primary movement match.
elif (
movement_to_lane
== next_edge
):
lane_candidates.append(
movement_id
)
# Only accept an unambiguous match.
if len(lane_candidates) == 1:
matched_movement = (
lane_candidates[0]
)
# =================================================
# METHOD 2
# Unique from-edge + to-edge matching.
# =================================================
if matched_movement is None:
if next_edge is not None:
candidates = (
edge_lookup.get(
(
from_edge,
next_edge
),
[]
)
)
# Only accept unique matches.
if len(candidates) == 1:
matched_movement = (
candidates[0]
)
# =================================================
# METHOD 3
# No safe movement assignment.
# =================================================
if matched_movement is None:
continue
# ------------------------------------------------
# Increment movement demand.
# ------------------------------------------------
if matched_movement in demand:
demand[
matched_movement
] += 1
except (
traci.TraCIException,
KeyError,
IndexError,
TypeError,
ValueError
):
# A vehicle may disappear between TraCI calls.
# Ignore that vehicle rather than stopping the
# complete controller.
continue
except Exception:
# Keep the demand layer robust against malformed
# vehicle state.
continue
return demand
# ============================================================
# PHASE DEMAND
# ============================================================
def calculate_phase_demand(
self,
phases,
movement_demand
):
"""
Calculate demand for each signal phase.
Parameters
----------
phases:
Iterable of phase objects.
A phase is expected to contain either:
phase_id
movement_ids
or equivalent attributes.
movement_demand:
Dictionary returned by calculate_movement_demand().
Returns
-------
dict
{
phase_id: total_demand
}
"""
phase_demand = {}
for phase in phases:
phase_id = (
phase.phase_id
)
movement_ids = (
getattr(
phase,
"movement_ids",
[]
)
)
total = 0
for movement_id in movement_ids:
total += movement_demand.get(
movement_id,
0
)
phase_demand[
phase_id
] = total
return phase_demand
# ============================================================
# NORMALIZE PHASE DEMAND
# ============================================================
def normalize_phase_demand(
self,
phase_demand
):
"""
Normalize phase demand to [0, 1].
The phase with the highest demand receives 1.0.
If every phase has zero demand, every normalized phase
receives 0.0.
"""
if not phase_demand:
return {}
maximum = max(
phase_demand.values()
)
if maximum <= 0:
return {
phase_id: 0.0
for phase_id in phase_demand
}
normalized = {}
for phase_id, demand in (
phase_demand.items()
):
value = (
float(demand)
/ float(maximum)
)
# Safety clamp.
value = max(
0.0,
min(
1.0,
value
)
)
normalized[
phase_id
] = value
return normalized
# ================================================================
# SIMPLE TEST
# ================================================================
def main():
print()
print("=" * 70)
print("SUMO DEMAND CALCULATOR TEST")
print("=" * 70)
print()
print(
"This module is intended to be imported by the"
)
print(
"integration controller after SUMO is running."
)
print()
print(
"Available class:"
)
print(
" SUMODemandCalculator"
)
print()
print(
"Methods:"
)
print(
" calculate_movement_demand()"
)
print(
" calculate_phase_demand()"
)
print(
" normalize_phase_demand()"
)
print()
if __name__ == "__main__":
main()