From 3623b4c9997c8dd0a764d2e3ec937c124afaff2c Mon Sep 17 00:00:00 2001 From: legend5teve <2659618982@qq.com> Date: Thu, 5 Mar 2026 16:05:42 -0700 Subject: [PATCH 1/4] refactor: Change scenario prompts in agents/scenarios.py --- agentevac/agents/scenarios.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/agentevac/agents/scenarios.py b/agentevac/agents/scenarios.py index eb8772d..41fe2a2 100644 --- a/agentevac/agents/scenarios.py +++ b/agentevac/agents/scenarios.py @@ -228,14 +228,24 @@ def scenario_prompt_suffix(mode: str) -> str: if cfg["mode"] == "no_notice": return ( "This is a no-notice wildfire scenario: do not assume official route instructions exist. " - "Rely mainly on subjective_information, inbox messages, and your own caution." + "Rely mainly on subjective_information, inbox messages, and your own caution. " + "Do NOT invent official instructions. Base decisions on environmental cues (smoke/flames/visibility), " + "your current hazard or forecast inputs if provided, and peer-to-peer messages. Seek credible info when available " + ", and choose conservative actions if uncertain." ) if cfg["mode"] == "alert_guided": return ( "This is an alert-guided scenario: official alerts describe the fire, but they do not prescribe a route. " - "Use forecast and hazard cues, but make your own navigation choice." + # "Use forecast and hazard cues, but make your own navigation choice." + "but do not prescribe a specific route. Do NOT invent route guidance. Use the provided official alert content, " + "hazard and forecast cues (if provided), and local road conditions to choose when, where and how to evacuate." + ) return ( "This is an advice-guided scenario: official alerts include route-oriented guidance. " - "You may use advisories, briefings, and expected utility as formal support." + "You may use advisories, briefings, and expected utility as formal support. " + # "ADVICE-GUIDED scenario: officials issue an evacuation *order* (leave immediately) and include route-oriented guidance (may be high-level and may change)." + "Default to following designated routes/instructions unless they are blocked, unsafe " + "or extremely congested; if deviating, state why and pick the safest feasible alternative. Stay responsive to updates." + ) From df2f05649156028aab1c0494692b1487fd94cac8 Mon Sep 17 00:00:00 2001 From: legend5teve <2659618982@qq.com> Date: Thu, 5 Mar 2026 22:03:25 -0700 Subject: [PATCH 2/4] chore: update the suggested routes and destination for Lytton Changes to be committed: modified: agentevac/simulation/main.py modified: agentevac/simulation/spawn_events.py modified: agentevac/utils/replay.py modified: sumo/Repaired.netecfg modified: sumo/Repaired.sumocfg --- agentevac/simulation/main.py | 326 ++++-------------- agentevac/simulation/spawn_events.py | 480 ++++++++++++++------------- agentevac/utils/replay.py | 82 ++++- sumo/Repaired.netecfg | 2 +- sumo/Repaired.sumocfg | 2 +- 5 files changed, 370 insertions(+), 522 deletions(-) diff --git a/agentevac/simulation/main.py b/agentevac/simulation/main.py index 3c5f7b8..72c9d5b 100644 --- a/agentevac/simulation/main.py +++ b/agentevac/simulation/main.py @@ -68,6 +68,7 @@ from agentevac.agents.departure_model import should_depart_now from agentevac.agents.routing_utility import annotate_menu_with_expected_utility from agentevac.analysis.metrics import RunMetricsCollector +from agentevac.simulation.spawn_events import SPAWN_EVENTS from agentevac.utils.forecast_layer import ( build_fire_forecast, estimate_edge_forecast_risk, @@ -118,13 +119,34 @@ # Preset routes (Situation 1) - only needed if CONTROL_MODE="route" ROUTE_LIBRARY = [ - # {"name": "route_0", "edges": ["edgeA", "edgeB", "edgeC"]}, + {"name": "route_0", "edges": ["-479435809#1", + "-479435809#0", + "-479435812#0", + "-479435806", + "-30689314#10", + "-30689314#9", + "-30689314#8", + "-30689314#7", + "-30689314#6", + "-30689314#5", + "-30689314#4", + "-30689314#1", + "-30689314#0", + "-479505716#1", + "-479505717", + "-479505352", + "-479505354#2", + "-479505354#1", + "-479505354#0", + "-42047741#0", + "E#S1" + ]}, ] # Preset destinations (Situation 2) DESTINATION_LIBRARY = [ {"name": "shelter_0", "edge": "-42006543#0"}, - {"name": "shelter_1", "edge": "-42047741#0"}, + {"name": "shelter_1", "edge": "E#S1"}, {"name": "shelter_2", "edge": "42044784#5"}, ] @@ -1036,266 +1058,6 @@ def cleanup(self, active_vehicle_ids: List[str]): self._poi_by_vehicle.pop(vid, None) self._last_label.pop(vid, None) -C_RED = (255, 0, 0, 255) # Red, Green, Blue, Alpha -C_ORANGE = (255, 125, 0, 255) -C_YELLOW = (255, 255, 0, 255) -C_SPRING = (125, 255, 0, 255) -C_GREEN = (0, 255, 0, 255) -C_CYAN = (0, 255, 255, 255) -C_OCEAN = (0, 125, 255, 255) -C_BLUE = (0, 0, 255, 255) -C_VIOLET = (125, 0, 255, 255) -C_MAGENTA = (255, 0, 255, 255) -# ---- Scenario spawn events (time in seconds) ---- -SPAWN_EVENTS = [ - # vehicle id, spawn edge, dest edge (initial), depart time, lane, pos, speed, (color) - ("veh1_1", "42006672", "-42047741#0", 0.0, "first", "10", "max", C_RED), - ("veh1_2", "42006672", "-42047741#0", 5.0, "first", "10", "max", C_BLUE), - ("veh1_3", "42006672", "-42047741#0", 10.0, "first", "10", "max", C_GREEN), - - ("veh2_1", "42006514#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - ("veh2_2", "42006514#4", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - - ("veh3_1", "-42006515", "-42047741#0", 0.0, "first", "20", "max", C_RED), - ("veh3_2", "-42006515", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - - ("veh4_1", "42006515", "-42047741#0", 0.0, "first", "20", "max", C_RED), - ("veh4_2", "42006515", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - - ("veh5_1", "42006565", "-42047741#0", 0.0, "first", "20", "max", C_RED), - ("veh5_2", "42006565", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - - # ("veh6_1", "-42006513#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh6_2", "-42006513#0", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh6_3", "-42006513#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh6_4", "-42006513#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh6_5", "-42006513#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh7_1", "42006504#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh7_2", "42006504#1", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh7_3", "42006504#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh8_1", "42006513#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh8_2", "42006513#0", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh8_3", "42006513#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh8_4", "42006513#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh8_5", "42006513#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh9_1", "-42006719#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh9_2", "-42006719#1", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh9_3", "-42006719#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh9_4", "-42006719#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh9_5", "-42006719#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh9_6", "-42006719#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh9_7", "-42006719#1", "-42047741#0", 30.0, "first", "20", "max", C_YELLOW), - # - # ("veh10_1", "42006513#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh10_2", "42006513#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh10_3", "42006513#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh10_4", "42006513#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh10_5", "42006513#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh10_6", "42006513#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # - # ("veh11_1", "-42006513#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh11_2", "-42006513#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh11_3", "-42006513#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh11_4", "-42006513#2", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh11_5", "-42006513#2", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh11_6", "-42006513#2", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh11_7", "-42006513#2", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh11_8", "-42006513#2", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # ("veh11_9", "-42006513#2", "-42047741#0", 40.0, "first", "20", "max", C_MAGENTA), - # - # ("veh12_1", "30689314#5", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh12_2", "30689314#5", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh12_3", "30689314#5", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh12_4", "30689314#5", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh13_1", "-30689314#5", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh13_2", "-30689314#5", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh13_3", "-30689314#5", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh13_4", "-30689314#5", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh13_5", "-30689314#5", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh13_6", "-30689314#5", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # - # ("veh14_1", "42006513#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh14_2", "42006513#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh14_3", "42006513#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh14_4", "42006513#2", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh14_5", "42006513#2", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh14_6", "42006513#2", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh14_7", "42006513#2", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh14_8", "42006513#2", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # ("veh14_9", "42006513#2", "-42047741#0", 40.0, "first", "20", "max", C_MAGENTA), - # - # ("veh15_1", "-30689314#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh15_2", "-30689314#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh15_3", "-30689314#4", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh15_4", "-30689314#4", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh15_5", "-30689314#4", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh15_6", "-30689314#4", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh15_7", "-30689314#4", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # - # ("veh16_1", "-42006513#3", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh16_2", "-42006513#3", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh16_3", "-42006513#3", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh16_4", "-42006513#3", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh16_5", "-42006513#3", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh17_1", "42006513#3", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh17_2", "42006513#3", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh17_3", "42006513#3", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh18_1", "42006734#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh18_2", "42006734#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh18_3", "42006734#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh18_4", "42006734#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh18_5", "42006734#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh18_6", "42006734#0", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh18_7", "42006734#0", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh18_8", "42006734#0", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # - # ("veh19_1", "-42006513#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh19_2", "-42006513#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh19_3", "-42006513#4", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh20_1", "42006513#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh20_2", "42006513#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh21_1", "30689314#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh21_2", "30689314#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh21_3", "30689314#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh21_4", "30689314#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh22_1", "-30689314#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh22_2", "-30689314#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh22_3", "-30689314#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh22_4", "-30689314#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh23_1", "42006734#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh23_2", "42006734#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh23_3", "42006734#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh23_4", "42006734#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh24_1", "42006713#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh24_2", "42006713#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh24_3", "42006713#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh25_1", "42006701#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh25_2", "42006701#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh25_3", "42006701#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh25_4", "42006701#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh25_5", "42006701#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh26_1", "479505716#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh26_2", "479505716#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh26_3", "479505716#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh26_4", "479505716#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh27_1", "-479505716#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh27_2", "-479505716#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh27_3", "-479505716#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh27_4", "-479505716#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh28_1", "42006734#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh28_2", "42006734#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh28_3", "42006734#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh29_1", "42006734#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh29_2", "42006734#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh30_1", "-42006522#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh30_2", "-42006522#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh30_3", "-42006522#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh31_1", "42006522#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh31_2", "42006522#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh32_1", "42006636#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh32_2", "42006636#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh32_3", "42006636#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh32_4", "42006636#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh32_5", "42006636#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh33_1", "-966804140", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh33_2", "-966804140", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh33_3", "-966804140", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh34_1", "42006708", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh34_2", "42006708", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh34_3", "42006708", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh35_1", "479505354#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh35_2", "479505354#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh35_3", "479505354#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh36_1", "-42006660", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh36_2", "-42006660", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh36_3", "-42006660", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh36_4", "-42006660", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh36_5", "-42006660", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh37_1", "42006589", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh37_2", "42006589", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh37_3", "42006589", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh38_1", "42006572", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh38_2", "42006572", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh39_1", "42006733", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh39_2", "42006733", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh39_3", "42006733", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh40_1", "42006506", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh40_2", "42006506", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh40_3", "42006506", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh40_4", "42006506", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh40_5", "42006506", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh41_1", "-42006549#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh41_2", "-42006549#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh41_3", "-42006549#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh41_4", "-42006549#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh41_5", "-42006549#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh41_6", "-42006549#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh41_7", "-42006549#1", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh41_8", "-42006549#1", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # - # ("veh42_1", "-42006552#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh42_2", "-42006552#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh42_3", "-42006552#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh42_4", "-42006552#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh42_5", "-42006552#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh43_1", "42006552#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh43_2", "42006552#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh43_3", "42006552#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh43_4", "42006552#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh43_5", "42006552#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh44_1", "-42006552#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh44_2", "-42006552#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh44_3", "-42006552#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh44_4", "-42006552#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh44_5", "-42006552#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh45_1", "-42006706#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh45_2", "-42006706#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh45_3", "-42006706#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh45_4", "-42006706#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh45_5", "-42006706#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh46_1", "42006706#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh46_2", "42006706#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh46_3", "42006706#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh46_4", "42006706#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh46_5", "42006706#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh46_6", "42006706#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh46_7", "42006706#1", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # - # ("veh47_1", "42006592", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh47_2", "42006592", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), -] spawned = set() @@ -1334,6 +1096,9 @@ def cleanup(self, active_vehicle_ids: List[str]): id_label_max=OVERLAY_ID_LABEL_MAX, ) print(f"[REPLAY] mode={RUN_MODE} path={replay.path}") +if RUN_MODE == "replay": + departure_source = "recorded_departure_events" if replay.has_departure_schedule() else "spawn_events_fallback" + print(f"[REPLAY_DEPARTURES] source={departure_source}") if replay.dialog_path: print(f"[DIALOG] path={replay.dialog_path}") if replay.dialog_csv_path: @@ -1887,15 +1652,20 @@ def process_pending_departures(step_idx: int): on decision ticks (multiples of ``decision_period_steps``); all other steps return immediately after checking whether any vehicle's scheduled depart time has passed. - For each not-yet-spawned vehicle whose depart time has been reached: + In record mode, all spawn events become eligible from simulation time 0 so the + actual release time is governed by the departure model rather than the static + ``t0`` values in ``SPAWN_EVENTS``. + + For each not-yet-spawned vehicle whose release gate has been reached: 1. Samples a noisy/delayed environment signal for the spawn edge. 2. Builds a social signal (empty inbox for pre-departure agents). 3. Updates the Bayesian belief distribution. 4. Evaluates the three-clause departure decision rule. 5. If departing, adds the vehicle to the SUMO simulation via TraCI. - In replay mode, vehicles are added immediately when their depart time is reached - without running the departure decision logic. + In replay mode, vehicles are added when the recorded ``departure_release`` event + for that vehicle is encountered. If the replay log predates departure-event + logging, the function falls back to the static ``SPAWN_EVENTS`` schedule. Args: step_idx: The current SUMO simulation step index. @@ -1922,12 +1692,19 @@ def forecast_edge_risk(edge_id: str) -> Tuple[bool, float, float]: for (vid, from_edge, to_edge, t0, dLane, dPos, dSpeed, dColor) in SPAWN_EVENTS: if vid in spawned: continue - if sim_t < t0: - continue if RUN_MODE == "replay": - should_release = True - release_reason = "replay_schedule" + departure_rec = replay.departure_record_for_step(step_idx, vid) + if departure_rec is not None: + should_release = True + release_reason = str(departure_rec.get("reason") or "replay_recorded_departure") + else: + if replay.has_departure_schedule(): + continue + if sim_t < t0: + continue + should_release = True + release_reason = "replay_schedule_fallback" agent_state = ensure_agent_state( vid, sim_t, @@ -1939,6 +1716,9 @@ def forecast_edge_risk(edge_id: str) -> Tuple[bool, float, float]: default_lambda_t=DEFAULT_LAMBDA_T, ) else: + effective_t0 = 0.0 + if sim_t < effective_t0: + continue if not evaluate_departures: continue @@ -2106,6 +1886,14 @@ def forecast_edge_risk(edge_id: str) -> Tuple[bool, float, float]: traci.vehicle.setColor(vid, dColor) spawned.add(vid) agent_state.has_departed = True + replay.record_departure_release( + step=step_idx, + sim_t_s=sim_t, + veh_id=vid, + from_edge=from_edge, + to_edge=to_edge, + reason=release_reason, + ) metrics.record_departure(vid, sim_t, release_reason) print(f"[DEPART] {vid}: released from {from_edge} via {release_reason}") if EVENTS_ENABLED: diff --git a/agentevac/simulation/spawn_events.py b/agentevac/simulation/spawn_events.py index ec5bd1b..4d2debd 100644 --- a/agentevac/simulation/spawn_events.py +++ b/agentevac/simulation/spawn_events.py @@ -16,17 +16,27 @@ - ``lane`` : SUMO departure lane specifier (e.g., ``"first"``). - ``pos`` : Departure position on the edge in metres. - ``speed`` : Departure speed (``"max"`` uses the lane speed limit). - - ``color`` : RGBA color constant defined in ``agentevac/simulation/main.py`` - (e.g., ``C_RED``, ``C_BLUE``). + - ``color`` : RGBA color tuple used for the initial SUMO vehicle color. -Active groups (veh1–veh5): 12 vehicles across 5 spawn locations; the baseline +Active groups (veh1-veh5): 12 vehicles across 5 spawn locations; the baseline scenario for development and testing. -Commented-out groups (veh6–veh47): Additional spawn locations across the road network. +Commented-out groups (veh6-veh47): Additional spawn locations across the road network. Disabled to keep the active agent count manageable. Re-enable individual groups to test denser evacuation scenarios. """ +C_RED = (255, 0, 0, 255) +C_ORANGE = (255, 125, 0, 255) +C_YELLOW = (255, 255, 0, 255) +C_SPRING = (125, 255, 0, 255) +C_GREEN = (0, 255, 0, 255) +C_CYAN = (0, 255, 255, 255) +C_OCEAN = (0, 125, 255, 255) +C_BLUE = (0, 0, 255, 255) +C_VIOLET = (125, 0, 255, 255) +C_MAGENTA = (255, 0, 255, 255) + SPAWN_EVENTS = [ # vehicle id, spawn edge, dest edge (initial), depart time, lane, pos, speed, (color) ("veh1_1", "42006672", "-42047741#0", 0.0, "first", "10", "max", C_RED), @@ -45,234 +55,234 @@ ("veh5_1", "42006565", "-42047741#0", 0.0, "first", "20", "max", C_RED), ("veh5_2", "42006565", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh6_1", "-42006513#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh6_2", "-42006513#0", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh6_3", "-42006513#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh6_4", "-42006513#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh6_5", "-42006513#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh7_1", "42006504#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh7_2", "42006504#1", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh7_3", "42006504#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh8_1", "42006513#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh8_2", "42006513#0", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh8_3", "42006513#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh8_4", "42006513#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh8_5", "42006513#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh9_1", "-42006719#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh9_2", "-42006719#1", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), - # ("veh9_3", "-42006719#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh9_4", "-42006719#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh9_5", "-42006719#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh9_6", "-42006719#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh9_7", "-42006719#1", "-42047741#0", 30.0, "first", "20", "max", C_YELLOW), - # - # ("veh10_1", "42006513#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh10_2", "42006513#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh10_3", "42006513#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh10_4", "42006513#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh10_5", "42006513#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh10_6", "42006513#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # - # ("veh11_1", "-42006513#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh11_2", "-42006513#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh11_3", "-42006513#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh11_4", "-42006513#2", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh11_5", "-42006513#2", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh11_6", "-42006513#2", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh11_7", "-42006513#2", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh11_8", "-42006513#2", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # ("veh11_9", "-42006513#2", "-42047741#0", 40.0, "first", "20", "max", C_MAGENTA), - # - # ("veh12_1", "30689314#5", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh12_2", "30689314#5", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh12_3", "30689314#5", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh12_4", "30689314#5", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh13_1", "-30689314#5", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh13_2", "-30689314#5", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh13_3", "-30689314#5", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh13_4", "-30689314#5", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh13_5", "-30689314#5", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh13_6", "-30689314#5", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # - # ("veh14_1", "42006513#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh14_2", "42006513#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh14_3", "42006513#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh14_4", "42006513#2", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh14_5", "42006513#2", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh14_6", "42006513#2", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh14_7", "42006513#2", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh14_8", "42006513#2", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # ("veh14_9", "42006513#2", "-42047741#0", 40.0, "first", "20", "max", C_MAGENTA), - # - # ("veh15_1", "-30689314#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh15_2", "-30689314#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh15_3", "-30689314#4", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh15_4", "-30689314#4", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh15_5", "-30689314#4", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh15_6", "-30689314#4", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh15_7", "-30689314#4", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # - # ("veh16_1", "-42006513#3", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh16_2", "-42006513#3", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh16_3", "-42006513#3", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh16_4", "-42006513#3", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh16_5", "-42006513#3", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh17_1", "42006513#3", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh17_2", "42006513#3", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh17_3", "42006513#3", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh18_1", "42006734#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh18_2", "42006734#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh18_3", "42006734#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh18_4", "42006734#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh18_5", "42006734#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh18_6", "42006734#0", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh18_7", "42006734#0", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh18_8", "42006734#0", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # - # ("veh19_1", "-42006513#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh19_2", "-42006513#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh19_3", "-42006513#4", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh20_1", "42006513#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh20_2", "42006513#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh21_1", "30689314#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh21_2", "30689314#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh21_3", "30689314#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh21_4", "30689314#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh22_1", "-30689314#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh22_2", "-30689314#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh22_3", "-30689314#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh22_4", "-30689314#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh23_1", "42006734#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh23_2", "42006734#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh23_3", "42006734#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh23_4", "42006734#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh24_1", "42006713#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh24_2", "42006713#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh24_3", "42006713#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh25_1", "42006701#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh25_2", "42006701#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh25_3", "42006701#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh25_4", "42006701#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh25_5", "42006701#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh26_1", "479505716#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh26_2", "479505716#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh26_3", "479505716#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh26_4", "479505716#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh27_1", "-479505716#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh27_2", "-479505716#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh27_3", "-479505716#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh27_4", "-479505716#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # - # ("veh28_1", "42006734#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh28_2", "42006734#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh28_3", "42006734#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh29_1", "42006734#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh29_2", "42006734#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh30_1", "-42006522#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh30_2", "-42006522#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh30_3", "-42006522#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh31_1", "42006522#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh31_2", "42006522#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh32_1", "42006636#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh32_2", "42006636#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh32_3", "42006636#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh32_4", "42006636#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh32_5", "42006636#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh33_1", "-966804140", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh33_2", "-966804140", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh33_3", "-966804140", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh34_1", "42006708", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh34_2", "42006708", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh34_3", "42006708", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh35_1", "479505354#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh35_2", "479505354#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh35_3", "479505354#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh36_1", "-42006660", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh36_2", "-42006660", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh36_3", "-42006660", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh36_4", "-42006660", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh36_5", "-42006660", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh37_1", "42006589", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh37_2", "42006589", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh37_3", "42006589", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh38_1", "42006572", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh38_2", "42006572", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # - # ("veh39_1", "42006733", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh39_2", "42006733", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh39_3", "42006733", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # - # ("veh40_1", "42006506", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh40_2", "42006506", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh40_3", "42006506", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh40_4", "42006506", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh40_5", "42006506", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh41_1", "-42006549#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh41_2", "-42006549#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh41_3", "-42006549#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh41_4", "-42006549#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh41_5", "-42006549#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh41_6", "-42006549#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh41_7", "-42006549#1", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # ("veh41_8", "-42006549#1", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), - # - # ("veh42_1", "-42006552#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh42_2", "-42006552#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh42_3", "-42006552#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh42_4", "-42006552#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh42_5", "-42006552#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh43_1", "42006552#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh43_2", "42006552#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh43_3", "42006552#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh43_4", "42006552#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh43_5", "42006552#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh44_1", "-42006552#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh44_2", "-42006552#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh44_3", "-42006552#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh44_4", "-42006552#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh44_5", "-42006552#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh45_1", "-42006706#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh45_2", "-42006706#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh45_3", "-42006706#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh45_4", "-42006706#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh45_5", "-42006706#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # - # ("veh46_1", "42006706#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh46_2", "42006706#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), - # ("veh46_3", "42006706#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), - # ("veh46_4", "42006706#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), - # ("veh46_5", "42006706#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), - # ("veh46_6", "42006706#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), - # ("veh46_7", "42006706#1", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), - # - # ("veh47_1", "42006592", "-42047741#0", 0.0, "first", "20", "max", C_RED), - # ("veh47_2", "42006592", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), -] \ No newline at end of file + ("veh6_1", "-42006513#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh6_2", "-42006513#0", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), + ("veh6_3", "-42006513#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh6_4", "-42006513#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh6_5", "-42006513#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh7_1", "42006504#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh7_2", "42006504#1", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), + ("veh7_3", "42006504#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh8_1", "42006513#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh8_2", "42006513#0", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), + ("veh8_3", "42006513#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh8_4", "42006513#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh8_5", "42006513#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh9_1", "-42006719#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh9_2", "-42006719#1", "-42047741#0", 5.0, "first", "20", "max", C_BLUE), + ("veh9_3", "-42006719#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh9_4", "-42006719#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh9_5", "-42006719#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh9_6", "-42006719#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh9_7", "-42006719#1", "-42047741#0", 30.0, "first", "20", "max", C_YELLOW), + + ("veh10_1", "42006513#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh10_2", "42006513#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh10_3", "42006513#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh10_4", "42006513#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh10_5", "42006513#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh10_6", "42006513#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + + ("veh11_1", "-42006513#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh11_2", "-42006513#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh11_3", "-42006513#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh11_4", "-42006513#2", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh11_5", "-42006513#2", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh11_6", "-42006513#2", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh11_7", "-42006513#2", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), + ("veh11_8", "-42006513#2", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), + ("veh11_9", "-42006513#2", "-42047741#0", 40.0, "first", "20", "max", C_MAGENTA), + + ("veh12_1", "30689314#5", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh12_2", "30689314#5", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh12_3", "30689314#5", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh12_4", "30689314#5", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + + ("veh13_1", "-30689314#5", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh13_2", "-30689314#5", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh13_3", "-30689314#5", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh13_4", "-30689314#5", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh13_5", "-30689314#5", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh13_6", "-30689314#5", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + + ("veh14_1", "42006513#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh14_2", "42006513#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh14_3", "42006513#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh14_4", "42006513#2", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh14_5", "42006513#2", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh14_6", "42006513#2", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh14_7", "42006513#2", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), + ("veh14_8", "42006513#2", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), + ("veh14_9", "42006513#2", "-42047741#0", 40.0, "first", "20", "max", C_MAGENTA), + + ("veh15_1", "-30689314#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh15_2", "-30689314#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh15_3", "-30689314#4", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh15_4", "-30689314#4", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh15_5", "-30689314#4", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh15_6", "-30689314#4", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh15_7", "-30689314#4", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), + + ("veh16_1", "-42006513#3", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh16_2", "-42006513#3", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh16_3", "-42006513#3", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh16_4", "-42006513#3", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh16_5", "-42006513#3", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh17_1", "42006513#3", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh17_2", "42006513#3", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh17_3", "42006513#3", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh18_1", "42006734#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh18_2", "42006734#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh18_3", "42006734#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh18_4", "42006734#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh18_5", "42006734#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh18_6", "42006734#0", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh18_7", "42006734#0", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), + ("veh18_8", "42006734#0", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), + + ("veh19_1", "-42006513#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh19_2", "-42006513#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh19_3", "-42006513#4", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh20_1", "42006513#4", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh20_2", "42006513#4", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + + ("veh21_1", "30689314#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh21_2", "30689314#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh21_3", "30689314#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh21_4", "30689314#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + + ("veh22_1", "-30689314#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh22_2", "-30689314#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh22_3", "-30689314#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh22_4", "-30689314#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + + ("veh23_1", "42006734#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh23_2", "42006734#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh23_3", "42006734#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh23_4", "42006734#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + + ("veh24_1", "42006713#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh24_2", "42006713#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh24_3", "42006713#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh25_1", "42006701#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh25_2", "42006701#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh25_3", "42006701#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh25_4", "42006701#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh25_5", "42006701#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh26_1", "479505716#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh26_2", "479505716#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh26_3", "479505716#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh26_4", "479505716#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + + ("veh27_1", "-479505716#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh27_2", "-479505716#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh27_3", "-479505716#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh27_4", "-479505716#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + + ("veh28_1", "42006734#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh28_2", "42006734#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh28_3", "42006734#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh29_1", "42006734#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh29_2", "42006734#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + + ("veh30_1", "-42006522#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh30_2", "-42006522#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh30_3", "-42006522#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh31_1", "42006522#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh31_2", "42006522#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + + ("veh32_1", "42006636#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh32_2", "42006636#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh32_3", "42006636#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh32_4", "42006636#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh32_5", "42006636#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh33_1", "-966804140", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh33_2", "-966804140", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh33_3", "-966804140", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh34_1", "42006708", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh34_2", "42006708", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh34_3", "42006708", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh35_1", "479505354#2", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh35_2", "479505354#2", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh35_3", "479505354#2", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh36_1", "-42006660", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh36_2", "-42006660", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh36_3", "-42006660", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh36_4", "-42006660", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh36_5", "-42006660", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh37_1", "42006589", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh37_2", "42006589", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh37_3", "42006589", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh38_1", "42006572", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh38_2", "42006572", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + + ("veh39_1", "42006733", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh39_2", "42006733", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh39_3", "42006733", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + + ("veh40_1", "42006506", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh40_2", "42006506", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh40_3", "42006506", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh40_4", "42006506", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh40_5", "42006506", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh41_1", "-42006549#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh41_2", "-42006549#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh41_3", "-42006549#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh41_4", "-42006549#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh41_5", "-42006549#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh41_6", "-42006549#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh41_7", "-42006549#1", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), + ("veh41_8", "-42006549#1", "-42047741#0", 35.0, "first", "20", "max", C_VIOLET), + + ("veh42_1", "-42006552#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh42_2", "-42006552#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh42_3", "-42006552#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh42_4", "-42006552#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh42_5", "-42006552#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh43_1", "42006552#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh43_2", "42006552#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh43_3", "42006552#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh43_4", "42006552#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh43_5", "42006552#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh44_1", "-42006552#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh44_2", "-42006552#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh44_3", "-42006552#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh44_4", "-42006552#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh44_5", "-42006552#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh45_1", "-42006706#0", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh45_2", "-42006706#0", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh45_3", "-42006706#0", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh45_4", "-42006706#0", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh45_5", "-42006706#0", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + + ("veh46_1", "42006706#1", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh46_2", "42006706#1", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), + ("veh46_3", "42006706#1", "-42047741#0", 10.0, "first", "20", "max", C_GREEN), + ("veh46_4", "42006706#1", "-42047741#0", 15.0, "first", "20", "max", C_ORANGE), + ("veh46_5", "42006706#1", "-42047741#0", 20.0, "first", "20", "max", C_SPRING), + ("veh46_6", "42006706#1", "-42047741#0", 25.0, "first", "20", "max", C_CYAN), + ("veh46_7", "42006706#1", "-42047741#0", 30.0, "first", "20", "max", C_OCEAN), + + ("veh47_1", "42006592", "-42047741#0", 0.0, "first", "20", "max", C_RED), + ("veh47_2", "42006592", "-42047741#0", 5.0, "first", "20", "max", C_YELLOW), +] diff --git a/agentevac/utils/replay.py b/agentevac/utils/replay.py index 73cdf8d..1b4edaf 100644 --- a/agentevac/utils/replay.py +++ b/agentevac/utils/replay.py @@ -1,11 +1,13 @@ -"""Record and replay LLM-driven route decisions for deterministic re-runs. +"""Record and replay LLM-driven actions for deterministic re-runs. This module provides ``RouteReplay``, a class that operates in one of two modes: -**record** — During a live simulation run, every LLM-applied route change is logged +**record** — During a live simulation run, every replay-relevant action is logged to a JSONL file (one JSON record per line) along with agent cognition snapshots - and LLM dialog transcripts. Only ``route_change`` events are used for replay; - cognition and dialog events are write-only metadata for research/debugging. + and LLM dialog transcripts. Replay currently consumes: + - ``departure_release`` events for vehicle release timing + - ``route_change`` events for route application + Cognition and dialog events are write-only metadata for research/debugging. Three output files are created: - ``routes_.jsonl`` — Replayable route-change schedule. @@ -13,6 +15,7 @@ - ``routes_.dialogs.csv`` — Machine-readable LLM dialog table. **replay** — Loads a previously recorded JSONL file and, on each simulation step, + releases vehicles according to the recorded ``departure_release`` schedule and applies the scheduled ``route_change`` events to the matching vehicle via ``traci.vehicle.setRoute()``. This allows exact behavioural reproduction without making any OpenAI API calls. @@ -46,7 +49,8 @@ def __init__(self, mode: str, path: str): self._dialog_fh = None self._dialog_csv_fh = None self._dialog_csv_writer = None - self._schedule = {} # step_idx -> veh_id -> record + self._schedule = {} # step_idx -> veh_id -> route_change record + self._departure_schedule = {} # step_idx -> veh_id -> departure_release record if self.mode == "record": self.path = self._build_record_path(path) @@ -75,7 +79,7 @@ def __init__(self, mode: str, path: str): self._dialog_csv_writer.writeheader() self._dialog_csv_fh.flush() elif self.mode == "replay": - self._schedule = self._load_schedule(self.path) + self._schedule, self._departure_schedule = self._load_schedule(self.path) else: raise ValueError(f"Unknown RUN_MODE={mode}. Use 'record' or 'replay'.") @@ -108,32 +112,35 @@ def close(self): @staticmethod def _load_schedule(path: str): - """Load and index ``route_change`` events from a JSONL file by step index. + """Load replayable events from a JSONL file by step index. - Non-``route_change`` events (cognition, metrics snapshots, dialogs) are - silently ignored so replay only reproduces the route-assignment actions. + Replay currently consumes ``route_change`` and ``departure_release`` events. + All other events (cognition, metrics snapshots, dialogs) are silently ignored. Args: path: Path to the recorded JSONL file. Returns: - Dict mapping ``step_idx`` → {``veh_id`` → record dict}. + Tuple of dicts: + - ``route_schedule``: ``step_idx`` → {``veh_id`` → route-change record} + - ``departure_schedule``: ``step_idx`` → {``veh_id`` → departure record} """ - schedule = {} + route_schedule = {} + departure_schedule = {} with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue rec = json.loads(line) - # Only route-change events are replayable actions. event = rec.get("event", "route_change") - if event != "route_change": - continue step = int(rec["step"]) vid = rec["veh_id"] - schedule.setdefault(step, {})[vid] = rec - return schedule + if event == "route_change": + route_schedule.setdefault(step, {})[vid] = rec + elif event == "departure_release": + departure_schedule.setdefault(step, {})[vid] = rec + return route_schedule, departure_schedule @staticmethod def _build_record_path(base_path: str) -> str: @@ -239,6 +246,39 @@ def record_route_change( } self._write_jsonl(rec) + def record_departure_release( + self, + step: int, + sim_t_s: float, + veh_id: str, + from_edge: str, + to_edge: str, + reason: Optional[str] = None, + ): + """Log one vehicle release event to the JSONL file. + + This is replayable metadata used to reproduce the actual departure timing of + each vehicle in replay mode. + + Args: + step: SUMO simulation step index. + sim_t_s: Simulation time in seconds. + veh_id: Vehicle ID. + from_edge: Spawn edge used to initialize the route. + to_edge: Initial destination edge used to initialize the route. + reason: Optional departure reason. + """ + rec = { + "event": "departure_release", + "step": int(step), + "time_s": float(sim_t_s), + "veh_id": str(veh_id), + "from_edge": str(from_edge), + "to_edge": str(to_edge), + "reason": reason, + } + self._write_jsonl(rec) + def record_agent_cognition( self, step: int, @@ -271,6 +311,16 @@ def record_agent_cognition( } self._write_jsonl(rec) + def departure_record_for_step(self, step: int, veh_id: str) -> Optional[Dict[str, Any]]: + """Return the recorded departure-release event for one vehicle at one step.""" + if self.mode != "replay": + return None + return self._departure_schedule.get(int(step), {}).get(str(veh_id)) + + def has_departure_schedule(self) -> bool: + """Whether the loaded replay log contains explicit departure-release events.""" + return bool(self._departure_schedule) + def record_metric_snapshot( self, step: int, diff --git a/sumo/Repaired.netecfg b/sumo/Repaired.netecfg index 0365f81..125ab4b 100644 --- a/sumo/Repaired.netecfg +++ b/sumo/Repaired.netecfg @@ -1,6 +1,6 @@ - diff --git a/sumo/Repaired.sumocfg b/sumo/Repaired.sumocfg index 0360b58..636540a 100644 --- a/sumo/Repaired.sumocfg +++ b/sumo/Repaired.sumocfg @@ -1,6 +1,6 @@ - From 477b0dec20a793f354e6daeaf10d9e9479d20a1c Mon Sep 17 00:00:00 2001 From: legend5teve <2659618982@qq.com> Date: Thu, 5 Mar 2026 22:16:47 -0700 Subject: [PATCH 3/4] fix: update replay.py to fix key error Module updated: agentevac/utils/replay.py - Fixed RouteReplay._load_schedule(...) so it only reads step and veh_id for replayable events: - route_change - departure_release - Non-replayable events like agent_cognition and metrics_snapshot are now ignored without touching veh_id. Cause - The loader was accessing rec["veh_id"] before checking the event type. - metrics_snapshot records do not have veh_id, so replay loading crashed with KeyError. Verification 1. python3 -m py_compile agentevac/utils/replay.py passed. 2. Reproduced the failing case with a small local script: - one route_change - one agent_cognition - one metrics_snapshot - replay load now succeeds and only indexes the route-change step. --- agentevac/utils/replay.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/agentevac/utils/replay.py b/agentevac/utils/replay.py index 1b4edaf..c860604 100644 --- a/agentevac/utils/replay.py +++ b/agentevac/utils/replay.py @@ -134,11 +134,13 @@ def _load_schedule(path: str): continue rec = json.loads(line) event = rec.get("event", "route_change") - step = int(rec["step"]) - vid = rec["veh_id"] if event == "route_change": + step = int(rec["step"]) + vid = rec["veh_id"] route_schedule.setdefault(step, {})[vid] = rec elif event == "departure_release": + step = int(rec["step"]) + vid = rec["veh_id"] departure_schedule.setdefault(step, {})[vid] = rec return route_schedule, departure_schedule From 0f4ac33f9a118059399aee899f5bdac5f2e2ed6c Mon Sep 17 00:00:00 2001 From: legend5teve <2659618982@qq.com> Date: Mon, 9 Mar 2026 15:52:01 -0600 Subject: [PATCH 4/4] feat: optimize the visualization module for plotting statistic results and agent communication --- README.md | 43 +++++ pyproject.toml | 3 + scripts/_plot_common.py | 106 ++++++++++++ scripts/plot_agent_communication.py | 230 ++++++++++++++++++++++++++ scripts/plot_all_run_artifacts.py | 167 +++++++++++++++++++ scripts/plot_departure_timeline.py | 150 +++++++++++++++++ scripts/plot_experiment_comparison.py | 216 ++++++++++++++++++++++++ scripts/plot_run_metrics.py | 125 ++++++++++++++ 8 files changed, 1040 insertions(+) create mode 100644 scripts/_plot_common.py create mode 100644 scripts/plot_agent_communication.py create mode 100644 scripts/plot_all_run_artifacts.py create mode 100644 scripts/plot_departure_timeline.py create mode 100644 scripts/plot_experiment_comparison.py create mode 100644 scripts/plot_run_metrics.py diff --git a/README.md b/README.md index 40ffa64..e5db941 100644 --- a/README.md +++ b/README.md @@ -95,3 +95,46 @@ agentevac-study \ ``` This runs a grid search over information noise, delay, and trust parameters and fits results against a reference metrics file. + +## Plotting Completed Runs + +Install the plotting dependency: + +```bash +pip install -e .[plot] +``` + +Generate figures for the latest run: + +```bash +python3 scripts/plot_all_run_artifacts.py +``` + +Generate figures for a specific run ID: + +```bash +python3 scripts/plot_all_run_artifacts.py --run-id 20260309_030340 +``` + +Useful individual plotting commands: + +```bash +# 2x2 dashboard for one run_metrics_*.json +python3 scripts/plot_run_metrics.py --metrics outputs/run_metrics_20260309_030340.json + +# Departures, messages, system observations, and route changes over time +python3 scripts/plot_departure_timeline.py \ + --events outputs/events_20260309_030340.jsonl \ + --replay outputs/llm_routes_20260309_030340.jsonl + +# Messaging and dialog activity +python3 scripts/plot_agent_communication.py \ + --events outputs/events_20260309_030340.jsonl \ + --dialogs outputs/llm_routes_20260309_030340.dialogs.csv + +# Compare multiple completed runs or sweep outputs +python3 scripts/plot_experiment_comparison.py \ + --results-json outputs/experiments/experiment_results.json +``` + +By default, plots are saved under `outputs/figures/` or next to the selected input file. diff --git a/pyproject.toml b/pyproject.toml index ac40f89..a33c091 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,9 @@ dev = [ "mkdocs-material", "build", ] +plot = [ + "matplotlib>=3.8", +] [project.scripts] # Calibration / sweep tools expose a proper main() and work as CLI scripts. diff --git a/scripts/_plot_common.py b/scripts/_plot_common.py new file mode 100644 index 0000000..9f837ae --- /dev/null +++ b/scripts/_plot_common.py @@ -0,0 +1,106 @@ +"""Shared helpers for plotting completed simulation artifacts.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any, Iterable, List + + +def newest_file(pattern: str) -> Path: + """Return the newest file matching ``pattern``. + + Raises: + FileNotFoundError: If no matching files exist. + """ + matches = sorted(Path().glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True) + if not matches: + raise FileNotFoundError(f"No files match pattern: {pattern}") + return matches[0] + + +def resolve_input(path_arg: str | None, pattern: str) -> Path: + """Resolve an explicit input path or fall back to the newest matching file.""" + if path_arg: + path = Path(path_arg) + if not path.exists(): + raise FileNotFoundError(f"Input file does not exist: {path}") + return path + return newest_file(pattern) + + +def load_json(path: Path) -> Any: + """Load a JSON document from ``path``.""" + with path.open("r", encoding="utf-8") as fh: + return json.load(fh) + + +def load_jsonl(path: Path) -> List[dict[str, Any]]: + """Load JSON Lines from ``path`` into a list of dicts.""" + rows: List[dict[str, Any]] = [] + with path.open("r", encoding="utf-8") as fh: + for line in fh: + text = line.strip() + if not text: + continue + rows.append(json.loads(text)) + return rows + + +def ensure_output_path( + input_path: Path, + output_arg: str | None, + *, + suffix: str, +) -> Path: + """Resolve output path and ensure its parent directory exists.""" + if output_arg: + out = Path(output_arg) + else: + out = input_path.with_suffix("") + out = out.with_name(f"{out.name}.{suffix}.png") + out.parent.mkdir(parents=True, exist_ok=True) + return out + + +def top_items(mapping: dict[str, float], limit: int) -> list[tuple[str, float]]: + """Return up to ``limit`` items sorted by descending value then key.""" + items = sorted(mapping.items(), key=lambda item: (-item[1], item[0])) + return items[: max(1, int(limit))] + + +def bin_counts( + times_s: Iterable[float], + *, + bin_s: float, +) -> list[tuple[float, int]]: + """Bin event times into fixed-width buckets. + + Returns: + List of ``(bin_start_s, count)`` tuples in ascending order. + """ + counts: dict[float, int] = {} + width = max(float(bin_s), 1e-9) + for t in times_s: + bucket = width * int(float(t) // width) + counts[bucket] = counts.get(bucket, 0) + 1 + return sorted(counts.items(), key=lambda item: item[0]) + + +def require_matplotlib(): + """Import matplotlib lazily with a useful error message.""" + # Constrain thread-hungry numeric backends before importing matplotlib/numpy. + os.environ.setdefault("MPLBACKEND", "Agg") + os.environ.setdefault("OMP_NUM_THREADS", "1") + os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") + os.environ.setdefault("MKL_NUM_THREADS", "1") + os.environ.setdefault("NUMEXPR_NUM_THREADS", "1") + try: + import matplotlib.pyplot as plt + except ImportError as exc: + raise SystemExit( + "matplotlib is required for plotting. Install it with " + "`pip install -e .[plot]` or `pip install matplotlib`." + ) from exc + return plt diff --git a/scripts/plot_agent_communication.py b/scripts/plot_agent_communication.py new file mode 100644 index 0000000..6474639 --- /dev/null +++ b/scripts/plot_agent_communication.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Visualize agent-to-agent messaging and LLM dialog volume for one run.""" + +from __future__ import annotations + +import argparse +import csv +from pathlib import Path +from typing import Any + +try: + from scripts._plot_common import ensure_output_path, load_jsonl, require_matplotlib, resolve_input, top_items +except ModuleNotFoundError: + from _plot_common import ensure_output_path, load_jsonl, require_matplotlib, resolve_input, top_items + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Visualize messaging and dialog activity from events JSONL and dialogs CSV." + ) + parser.add_argument( + "--events", + help="Path to an events_*.jsonl file. Defaults to the newest outputs/events_*.jsonl.", + ) + parser.add_argument( + "--dialogs", + help="Path to a *.dialogs.csv file. Defaults to the newest outputs/*.dialogs.csv.", + ) + parser.add_argument( + "--out", + help="Output PNG path. Defaults to .communication.png.", + ) + parser.add_argument( + "--show", + action="store_true", + help="Open the figure window in addition to saving the PNG.", + ) + parser.add_argument( + "--top-n", + type=int, + default=15, + help="Maximum number of bars to draw in sender/recipient charts (default: 15).", + ) + return parser.parse_args() + + +def _load_dialog_rows(path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as fh: + return list(csv.DictReader(fh)) + + +def _draw_bar(ax, items: list[tuple[str, float]], title: str, ylabel: str, color: str) -> None: + if not items: + ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=11) + ax.set_title(title) + ax.set_axis_off() + return + labels = [k for k, _ in items] + values = [v for _, v in items] + ax.bar(range(len(values)), values, color=color) + ax.set_xticks(range(len(labels))) + ax.set_xticklabels(labels, rotation=60, ha="right", fontsize=8) + ax.set_title(title) + ax.set_ylabel(ylabel) + + +def _round_value(rec: dict[str, Any]) -> int | None: + for key in ("delivery_round", "deliver_round", "sent_round", "round"): + value = rec.get(key) + if value is None: + continue + try: + return int(value) + except (TypeError, ValueError): + continue + return None + + +def _plot_round_series(ax, event_rows: list[dict[str, Any]]) -> None: + series = { + "queued": {}, + "delivered": {}, + "llm": {}, + "predeparture": {}, + } + for rec in event_rows: + event = rec.get("event") + round_idx = _round_value(rec) + if round_idx is None: + continue + if event == "message_queued": + series["queued"][round_idx] = series["queued"].get(round_idx, 0) + 1 + elif event == "message_delivered": + series["delivered"][round_idx] = series["delivered"].get(round_idx, 0) + 1 + elif event == "llm_decision": + series["llm"][round_idx] = series["llm"].get(round_idx, 0) + 1 + elif event == "predeparture_llm_decision": + series["predeparture"][round_idx] = series["predeparture"].get(round_idx, 0) + 1 + + plotted = False + colors = { + "queued": "#4C78A8", + "delivered": "#54A24B", + "llm": "#F58518", + "predeparture": "#E45756", + } + for name, mapping in series.items(): + if not mapping: + continue + xs = sorted(mapping.keys()) + ys = [mapping[x] for x in xs] + ax.plot(xs, ys, marker="o", linewidth=1.8, label=name, color=colors[name]) + plotted = True + + if not plotted: + ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=11) + ax.set_axis_off() + return + ax.set_title("Message and Decision Volume by Round") + ax.set_xlabel("Decision Round") + ax.set_ylabel("Event Count") + ax.legend() + + +def _plot_dialog_modes(ax, dialog_rows: list[dict[str, str]]) -> None: + counts: dict[str, int] = {} + response_lengths: dict[str, list[int]] = {} + for row in dialog_rows: + mode = str(row.get("control_mode") or "unknown") + counts[mode] = counts.get(mode, 0) + 1 + response_text = row.get("response_text") or "" + response_lengths.setdefault(mode, []).append(len(response_text)) + + labels = sorted(counts.keys()) + if not labels: + ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=11) + ax.set_axis_off() + return + + xs = list(range(len(labels))) + count_vals = [counts[label] for label in labels] + avg_lens = [ + (sum(response_lengths[label]) / float(len(response_lengths[label]))) + if response_lengths[label] else 0.0 + for label in labels + ] + + ax.bar(xs, count_vals, color="#72B7B2", label="dialogs") + ax.set_xticks(xs) + ax.set_xticklabels(labels, rotation=20, ha="right") + ax.set_title("Dialog Volume and Avg Response Length") + ax.set_ylabel("Dialog Count") + + ax2 = ax.twinx() + ax2.plot(xs, avg_lens, color="#B279A2", marker="o", linewidth=1.8, label="avg response chars") + ax2.set_ylabel("Average Response Length (chars)") + + +def plot_agent_communication( + *, + events_path: Path, + dialogs_path: Path, + out_path: Path, + show: bool, + top_n: int, +) -> None: + plt = require_matplotlib() + event_rows = load_jsonl(events_path) + dialog_rows = _load_dialog_rows(dialogs_path) + + sender_counts: dict[str, int] = {} + recipient_counts: dict[str, int] = {} + for rec in event_rows: + event = rec.get("event") + if event == "message_queued": + sender = str(rec.get("from_id") or "unknown") + sender_counts[sender] = sender_counts.get(sender, 0) + 1 + elif event == "message_delivered": + recipient = str(rec.get("to_id") or "unknown") + recipient_counts[recipient] = recipient_counts.get(recipient, 0) + 1 + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle( + f"AgentEvac Communication Analysis\n{events_path.name} | {dialogs_path.name}", + fontsize=14, + ) + + _draw_bar( + axes[0, 0], + top_items({k: float(v) for k, v in sender_counts.items()}, top_n), + f"Top Message Senders (top {top_n})", + "Queued Messages", + "#4C78A8", + ) + _draw_bar( + axes[0, 1], + top_items({k: float(v) for k, v in recipient_counts.items()}, top_n), + f"Top Message Recipients (top {top_n})", + "Delivered Messages", + "#54A24B", + ) + _plot_round_series(axes[1, 0], event_rows) + _plot_dialog_modes(axes[1, 1], dialog_rows) + + fig.tight_layout(rect=(0, 0, 1, 0.95)) + fig.savefig(out_path, dpi=160, bbox_inches="tight") + print(f"[PLOT] events={events_path}") + print(f"[PLOT] dialogs={dialogs_path}") + print(f"[PLOT] output={out_path}") + if show: + plt.show() + plt.close(fig) + + +def main() -> None: + args = _parse_args() + events_path = resolve_input(args.events, "outputs/events_*.jsonl") + dialogs_path = resolve_input(args.dialogs, "outputs/*.dialogs.csv") + out_path = ensure_output_path(events_path, args.out, suffix="communication") + plot_agent_communication( + events_path=events_path, + dialogs_path=dialogs_path, + out_path=out_path, + show=args.show, + top_n=args.top_n, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_all_run_artifacts.py b/scripts/plot_all_run_artifacts.py new file mode 100644 index 0000000..68ad3ad --- /dev/null +++ b/scripts/plot_all_run_artifacts.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Generate all standard figures for one completed AgentEvac run.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +try: + from scripts._plot_common import newest_file + from scripts.plot_agent_communication import plot_agent_communication + from scripts.plot_departure_timeline import plot_timeline + from scripts.plot_experiment_comparison import load_cases, plot_experiment_comparison + from scripts.plot_run_metrics import plot_metrics_dashboard +except ModuleNotFoundError: + from _plot_common import newest_file + from plot_agent_communication import plot_agent_communication + from plot_departure_timeline import plot_timeline + from plot_experiment_comparison import load_cases, plot_experiment_comparison + from plot_run_metrics import plot_metrics_dashboard + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate the standard dashboard, timeline, comparison, and communication plots for one run." + ) + parser.add_argument("--run-id", help="Timestamp token such as 20260309_030340.") + parser.add_argument("--metrics", help="Explicit run_metrics JSON path.") + parser.add_argument("--events", help="Explicit events JSONL path.") + parser.add_argument("--replay", help="Explicit llm_routes JSONL path.") + parser.add_argument("--dialogs", help="Explicit dialogs CSV path.") + parser.add_argument( + "--results-json", + help="Optional experiment_results.json to also generate the multi-run comparison figure.", + ) + parser.add_argument( + "--out-dir", + help="Output directory. Defaults to outputs/figures//.", + ) + parser.add_argument("--show", action="store_true", help="Show figures interactively as they are generated.") + parser.add_argument("--top-n", type=int, default=15, help="Top-N bars for agent-level charts.") + parser.add_argument("--bin-s", type=float, default=30.0, help="Time-bin width in seconds for timeline counts.") + return parser.parse_args() + + +def _maybe_path(path_arg: str | None) -> Path | None: + if not path_arg: + return None + path = Path(path_arg) + if not path.exists(): + raise SystemExit(f"Input file does not exist: {path}") + return path + + +def _resolve_run_id(args: argparse.Namespace) -> str: + if args.run_id: + return str(args.run_id) + for path_arg in (args.events, args.metrics, args.replay, args.dialogs): + if path_arg: + match = re.search(r"(\d{8}_\d{6})", Path(path_arg).name) + if match: + return match.group(1) + newest = newest_file("outputs/events_*.jsonl") + stem = newest.stem + return stem.replace("events_", "", 1) + + +def _resolve_paths(args: argparse.Namespace, run_id: str) -> dict[str, Path | None]: + metrics = _maybe_path(args.metrics) + events = _maybe_path(args.events) + replay = _maybe_path(args.replay) + dialogs = _maybe_path(args.dialogs) + + if metrics is None: + candidate = Path(f"outputs/run_metrics_{run_id}.json") + metrics = candidate if candidate.exists() else newest_file("outputs/run_metrics_*.json") + if events is None: + candidate = Path(f"outputs/events_{run_id}.jsonl") + events = candidate if candidate.exists() else newest_file("outputs/events_*.jsonl") + if replay is None: + candidate = Path(f"outputs/llm_routes_{run_id}.jsonl") + replay = candidate if candidate.exists() else None + if dialogs is None: + candidate = Path(f"outputs/llm_routes_{run_id}.dialogs.csv") + dialogs = candidate if candidate.exists() else newest_file("outputs/*.dialogs.csv") + + return { + "metrics": metrics, + "events": events, + "replay": replay, + "dialogs": dialogs, + } + + +def main() -> None: + args = _parse_args() + run_id = _resolve_run_id(args) + paths = _resolve_paths(args, run_id) + + out_dir = Path(args.out_dir) if args.out_dir else Path("outputs/figures") / run_id + out_dir.mkdir(parents=True, exist_ok=True) + + metrics_path = paths["metrics"] + events_path = paths["events"] + replay_path = paths["replay"] + dialogs_path = paths["dialogs"] + assert metrics_path is not None + assert events_path is not None + assert dialogs_path is not None + + plot_metrics_dashboard( + metrics_path, + out_path=out_dir / "run_metrics.dashboard.png", + show=args.show, + top_n=args.top_n, + ) + plot_timeline( + events_path, + replay_path=replay_path, + out_path=out_dir / "run_timeline.png", + show=args.show, + bin_s=args.bin_s, + ) + plot_agent_communication( + events_path=events_path, + dialogs_path=dialogs_path, + out_path=out_dir / "agent_communication.png", + show=args.show, + top_n=args.top_n, + ) + comparison_source: Path | None = None + if args.results_json: + results_path = Path(args.results_json) + if not results_path.exists(): + raise SystemExit(f"Results JSON does not exist: {results_path}") + comparison_rows, comparison_source = load_cases(results_path, "outputs/run_metrics_*.json") + plot_experiment_comparison( + comparison_rows, + source_path=comparison_source, + out_path=out_dir / "experiment_comparison.png", + show=args.show, + ) + else: + metrics_matches = sorted(Path().glob("outputs/run_metrics_*.json")) + if len(metrics_matches) > 1: + comparison_rows, comparison_source = load_cases(None, "outputs/run_metrics_*.json") + plot_experiment_comparison( + comparison_rows, + source_path=comparison_source, + out_path=out_dir / "experiment_comparison.png", + show=args.show, + ) + + print(f"[PLOT] run_id={run_id}") + print(f"[PLOT] figures_dir={out_dir}") + print(f"[PLOT] metrics={metrics_path}") + print(f"[PLOT] events={events_path}") + if replay_path: + print(f"[PLOT] replay={replay_path}") + print(f"[PLOT] dialogs={dialogs_path}") + if comparison_source: + print(f"[PLOT] comparison_source={comparison_source}") + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_departure_timeline.py b/scripts/plot_departure_timeline.py new file mode 100644 index 0000000..5e8ab67 --- /dev/null +++ b/scripts/plot_departure_timeline.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Plot departure and communication timelines from completed simulation logs.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +try: + from scripts._plot_common import ( + bin_counts, + ensure_output_path, + load_jsonl, + require_matplotlib, + resolve_input, + ) +except ModuleNotFoundError: + from _plot_common import ( + bin_counts, + ensure_output_path, + load_jsonl, + require_matplotlib, + resolve_input, + ) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Visualize departures, messages, and route changes over time." + ) + parser.add_argument( + "--events", + help="Path to an events_*.jsonl file. Defaults to the newest outputs/events_*.jsonl.", + ) + parser.add_argument( + "--replay", + help="Optional llm_routes_*.jsonl replay log for route-change counts.", + ) + parser.add_argument( + "--out", + help="Output PNG path. Defaults to .timeline.png.", + ) + parser.add_argument( + "--show", + action="store_true", + help="Open the figure window in addition to saving the PNG.", + ) + parser.add_argument( + "--bin-s", + type=float, + default=30.0, + help="Time-bin width in seconds for event counts (default: 30).", + ) + return parser.parse_args() + + +def _extract_times(rows: list[dict], event_type: str) -> list[float]: + out = [] + for rec in rows: + if rec.get("event") == event_type and rec.get("time_s") is not None: + out.append(float(rec["time_s"])) + return sorted(out) + + +def _plot_cumulative(ax, times: list[float], title: str, color: str) -> None: + if not times: + ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=11) + ax.set_title(title) + ax.set_axis_off() + return + y = list(range(1, len(times) + 1)) + ax.step(times, y, where="post", color=color, linewidth=2) + ax.scatter(times, y, color=color, s=16) + ax.set_title(title) + ax.set_xlabel("Simulation Time (s)") + ax.set_ylabel("Cumulative Count") + + +def _plot_binned(ax, series: list[tuple[str, list[float], str]], *, bin_s: float) -> None: + plotted = False + for label, times, color in series: + binned = bin_counts(times, bin_s=bin_s) + if not binned: + continue + xs = [x for x, _ in binned] + ys = [y for _, y in binned] + ax.plot(xs, ys, marker="o", linewidth=1.8, label=label, color=color) + plotted = True + if not plotted: + ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=11) + ax.set_axis_off() + return + ax.set_title(f"Event Volume per {int(bin_s) if float(bin_s).is_integer() else bin_s}s Bin") + ax.set_xlabel("Simulation Time (s)") + ax.set_ylabel("Event Count") + ax.legend() + + +def plot_timeline(events_path: Path, *, replay_path: Path | None, out_path: Path, show: bool, bin_s: float) -> None: + plt = require_matplotlib() + event_rows = load_jsonl(events_path) + replay_rows = load_jsonl(replay_path) if replay_path else [] + + departure_times = _extract_times(event_rows, "departure_release") + message_times = _extract_times(event_rows, "message_delivered") + _extract_times(event_rows, "message_queued") + observation_times = _extract_times(event_rows, "system_observation_generated") + llm_times = _extract_times(event_rows, "llm_decision") + _extract_times(event_rows, "predeparture_llm_decision") + route_change_times = _extract_times(replay_rows, "route_change") + + fig, axes = plt.subplots(2, 1, figsize=(14, 9)) + fig.suptitle( + f"AgentEvac Timeline\n{events_path.name}" + (f" | replay={replay_path.name}" if replay_path else ""), + fontsize=14, + ) + + _plot_cumulative(axes[0], departure_times, "Cumulative Departures", "#E45756") + _plot_binned( + axes[1], + [ + ("Messages", sorted(message_times), "#4C78A8"), + ("System observations", sorted(observation_times), "#54A24B"), + ("LLM decisions", sorted(llm_times), "#F58518"), + ("Route changes", sorted(route_change_times), "#B279A2"), + ], + bin_s=bin_s, + ) + + fig.tight_layout(rect=(0, 0, 1, 0.95)) + fig.savefig(out_path, dpi=160, bbox_inches="tight") + print(f"[PLOT] events={events_path}") + if replay_path: + print(f"[PLOT] replay={replay_path}") + print(f"[PLOT] output={out_path}") + if show: + plt.show() + plt.close(fig) + + +def main() -> None: + args = _parse_args() + events_path = resolve_input(args.events, "outputs/events_*.jsonl") + replay_path = Path(args.replay) if args.replay else None + if replay_path and not replay_path.exists(): + raise SystemExit(f"Replay file does not exist: {replay_path}") + out_path = ensure_output_path(events_path, args.out, suffix="timeline") + plot_timeline(events_path, replay_path=replay_path, out_path=out_path, show=args.show, bin_s=args.bin_s) + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_experiment_comparison.py b/scripts/plot_experiment_comparison.py new file mode 100644 index 0000000..e63ba50 --- /dev/null +++ b/scripts/plot_experiment_comparison.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Compare multiple completed runs from an experiment sweep or metrics glob.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +try: + from scripts._plot_common import ensure_output_path, load_json, require_matplotlib +except ModuleNotFoundError: + from _plot_common import ensure_output_path, load_json, require_matplotlib + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Compare multiple AgentEvac runs from experiment_results.json or a metrics glob." + ) + parser.add_argument( + "--results-json", + help="Path to experiment_results.json from agentevac.analysis.experiments.", + ) + parser.add_argument( + "--metrics-glob", + default="outputs/run_metrics_*.json", + help="Glob of metrics JSON files used if --results-json is omitted " + "(default: outputs/run_metrics_*.json).", + ) + parser.add_argument( + "--out", + help="Output PNG path. Defaults to .comparison.png or outputs/metrics_comparison.png.", + ) + parser.add_argument( + "--show", + action="store_true", + help="Open the figure window in addition to saving the PNG.", + ) + return parser.parse_args() + + +def _safe_float(value: Any, default: float = 0.0) -> float: + try: + return float(value) + except (TypeError, ValueError): + return default + + +def _metrics_row(metrics: dict[str, Any]) -> dict[str, float]: + return { + "departure_variability": _safe_float(metrics.get("departure_time_variability")), + "route_entropy": _safe_float(metrics.get("route_choice_entropy")), + "hazard_exposure": _safe_float(metrics.get("average_hazard_exposure", {}).get("global_average")), + "avg_travel_time": _safe_float(metrics.get("average_travel_time", {}).get("average")), + "arrived_agents": _safe_float(metrics.get("arrived_agents")), + "departed_agents": _safe_float(metrics.get("departed_agents")), + } + + +def load_cases(results_json: Path | None, metrics_glob: str) -> tuple[list[dict[str, Any]], Path]: + rows: list[dict[str, Any]] = [] + if results_json is not None: + payload = load_json(results_json) + if not isinstance(payload, list): + raise SystemExit(f"Expected a list in {results_json}") + for item in payload: + metrics_path = item.get("metrics_path") + if not metrics_path: + continue + path = Path(str(metrics_path)) + if not path.exists(): + continue + metrics = load_json(path) + case = item.get("case") or {} + row = { + "label": str(item.get("case_id") or path.stem), + "scenario": str(case.get("scenario", "unknown")), + "info_sigma": _safe_float(case.get("info_sigma")), + "info_delay_s": _safe_float(case.get("info_delay_s")), + "theta_trust": _safe_float(case.get("theta_trust")), + "metrics_path": str(path), + } + row.update(_metrics_row(metrics)) + rows.append(row) + return rows, results_json + + matches = sorted(Path().glob(metrics_glob)) + if not matches: + raise SystemExit(f"No metrics files match pattern: {metrics_glob}") + for path in matches: + metrics = load_json(path) + row = { + "label": path.stem, + "scenario": "unknown", + "info_sigma": 0.0, + "info_delay_s": 0.0, + "theta_trust": 0.0, + "metrics_path": str(path), + } + row.update(_metrics_row(metrics)) + rows.append(row) + return rows, matches[-1] + + +def _scatter_by_scenario(ax, rows: list[dict[str, Any]]) -> None: + scenario_colors = { + "no_notice": "#E45756", + "alert_guided": "#F58518", + "advice_guided": "#4C78A8", + "unknown": "#777777", + } + seen = set() + for row in rows: + scenario = str(row.get("scenario", "unknown")) + label = scenario if scenario not in seen else None + seen.add(scenario) + size = max(30.0, 20.0 + 20.0 * row.get("theta_trust", 0.0)) + ax.scatter( + row["hazard_exposure"], + row["avg_travel_time"], + s=size, + color=scenario_colors.get(scenario, "#777777"), + alpha=0.85, + label=label, + ) + ax.set_title("Hazard Exposure vs Travel Time") + ax.set_xlabel("Global Hazard Exposure") + ax.set_ylabel("Average Travel Time (s)") + if seen: + ax.legend() + + +def _line_vs_sigma(ax, rows: list[dict[str, Any]]) -> None: + by_scenario: dict[str, list[dict[str, Any]]] = {} + for row in rows: + by_scenario.setdefault(str(row.get("scenario", "unknown")), []).append(row) + if not by_scenario: + ax.text(0.5, 0.5, "No data", ha="center", va="center") + ax.set_axis_off() + return + for scenario, scenario_rows in sorted(by_scenario.items()): + ordered = sorted(scenario_rows, key=lambda item: item.get("info_sigma", 0.0)) + xs = [r.get("info_sigma", 0.0) for r in ordered] + ys = [r.get("route_entropy", 0.0) for r in ordered] + ax.plot(xs, ys, marker="o", linewidth=1.8, label=scenario) + ax.set_title("Route Entropy vs Info Sigma") + ax.set_xlabel("INFO_SIGMA") + ax.set_ylabel("Route Choice Entropy") + ax.legend() + + +def _bar_mean_by_scenario(ax, rows: list[dict[str, Any]], field: str, title: str, ylabel: str, color: str) -> None: + groups: dict[str, list[float]] = {} + for row in rows: + groups.setdefault(str(row.get("scenario", "unknown")), []).append(float(row.get(field, 0.0))) + labels = sorted(groups.keys()) + if not labels: + ax.text(0.5, 0.5, "No data", ha="center", va="center") + ax.set_axis_off() + return + means = [sum(groups[label]) / float(len(groups[label])) for label in labels] + ax.bar(range(len(labels)), means, color=color) + ax.set_xticks(range(len(labels))) + ax.set_xticklabels(labels, rotation=20, ha="right") + ax.set_title(title) + ax.set_ylabel(ylabel) + + +def plot_experiment_comparison(rows: list[dict[str, Any]], *, source_path: Path, out_path: Path, show: bool) -> None: + plt = require_matplotlib() + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle( + f"AgentEvac Experiment Comparison\n{source_path.name} | runs={len(rows)}", + fontsize=14, + ) + + _scatter_by_scenario(axes[0, 0], rows) + _line_vs_sigma(axes[0, 1], rows) + _bar_mean_by_scenario( + axes[1, 0], + rows, + field="avg_travel_time", + title="Mean Travel Time by Scenario", + ylabel="Average Travel Time (s)", + color="#4C78A8", + ) + _bar_mean_by_scenario( + axes[1, 1], + rows, + field="hazard_exposure", + title="Mean Hazard Exposure by Scenario", + ylabel="Global Hazard Exposure", + color="#E45756", + ) + + fig.tight_layout(rect=(0, 0, 1, 0.95)) + fig.savefig(out_path, dpi=160, bbox_inches="tight") + print(f"[PLOT] source={source_path}") + print(f"[PLOT] output={out_path}") + if show: + plt.show() + plt.close(fig) + + +def main() -> None: + args = _parse_args() + results_path = Path(args.results_json) if args.results_json else None + if results_path and not results_path.exists(): + raise SystemExit(f"Results JSON does not exist: {results_path}") + rows, source_path = load_cases(results_path, args.metrics_glob) + out_path = ensure_output_path(source_path, args.out, suffix="comparison") + plot_experiment_comparison(rows, source_path=source_path, out_path=out_path, show=args.show) + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_run_metrics.py b/scripts/plot_run_metrics.py new file mode 100644 index 0000000..d09638e --- /dev/null +++ b/scripts/plot_run_metrics.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Plot a compact dashboard for one completed simulation metrics JSON.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +try: + from scripts._plot_common import ensure_output_path, load_json, require_matplotlib, resolve_input, top_items +except ModuleNotFoundError: + from _plot_common import ensure_output_path, load_json, require_matplotlib, resolve_input, top_items + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Visualize one run_metrics_*.json file as a 2x2 dashboard." + ) + parser.add_argument( + "--metrics", + help="Path to a metrics JSON file. Defaults to the newest outputs/run_metrics_*.json.", + ) + parser.add_argument( + "--out", + help="Output PNG path. Defaults to .dashboard.png.", + ) + parser.add_argument( + "--show", + action="store_true", + help="Open the figure window in addition to saving the PNG.", + ) + parser.add_argument( + "--top-n", + type=int, + default=20, + help="Maximum number of per-agent bars to draw in each panel (default: 20).", + ) + return parser.parse_args() + + +def _draw_or_empty(ax, items: list[tuple[str, float]], title: str, ylabel: str, color: str, *, highest_first: bool = True): + if not items: + ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=11) + ax.set_title(title) + ax.set_axis_off() + return + labels = [k for k, _ in items] + values = [v for _, v in items] + if not highest_first: + labels = list(reversed(labels)) + values = list(reversed(values)) + ax.bar(range(len(values)), values, color=color) + ax.set_xticks(range(len(labels))) + ax.set_xticklabels(labels, rotation=60, ha="right", fontsize=8) + ax.set_title(title) + ax.set_ylabel(ylabel) + + +def plot_metrics_dashboard(metrics_path: Path, *, out_path: Path, show: bool, top_n: int) -> None: + plt = require_matplotlib() + metrics = load_json(metrics_path) + + kpis = { + "Departure variance": float(metrics.get("departure_time_variability", 0.0)), + "Route entropy": float(metrics.get("route_choice_entropy", 0.0)), + "Hazard exposure": float(metrics.get("average_hazard_exposure", {}).get("global_average", 0.0)), + "Avg travel time": float(metrics.get("average_travel_time", {}).get("average", 0.0)), + } + exposure = metrics.get("average_hazard_exposure", {}).get("per_agent_average", {}) or {} + travel = metrics.get("average_travel_time", {}).get("per_agent", {}) or {} + instability = metrics.get("decision_instability", {}).get("per_agent_changes", {}) or {} + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle( + f"AgentEvac Run Metrics\n{metrics_path.name} | mode={metrics.get('run_mode', 'unknown')} " + f"| departed={metrics.get('departed_agents', 0)} | arrived={metrics.get('arrived_agents', 0)}", + fontsize=14, + ) + + axes[0, 0].bar(range(len(kpis)), list(kpis.values()), color=["#4C78A8", "#F58518", "#E45756", "#54A24B"]) + axes[0, 0].set_xticks(range(len(kpis))) + axes[0, 0].set_xticklabels(list(kpis.keys()), rotation=20, ha="right") + axes[0, 0].set_title("Run KPI Summary") + axes[0, 0].set_ylabel("Value") + + _draw_or_empty( + axes[0, 1], + top_items(travel, top_n), + f"Per-Agent Travel Time (top {top_n})", + "Seconds", + "#4C78A8", + ) + _draw_or_empty( + axes[1, 0], + top_items(exposure, top_n), + f"Per-Agent Hazard Exposure (top {top_n})", + "Average Risk Score", + "#E45756", + ) + _draw_or_empty( + axes[1, 1], + top_items({k: float(v) for k, v in instability.items()}, top_n), + f"Per-Agent Decision Instability (top {top_n})", + "Choice Changes", + "#72B7B2", + ) + + fig.tight_layout(rect=(0, 0, 1, 0.95)) + fig.savefig(out_path, dpi=160, bbox_inches="tight") + print(f"[PLOT] metrics={metrics_path}") + print(f"[PLOT] output={out_path}") + if show: + plt.show() + plt.close(fig) + + +def main() -> None: + args = _parse_args() + metrics_path = resolve_input(args.metrics, "outputs/run_metrics_*.json") + out_path = ensure_output_path(metrics_path, args.out, suffix="dashboard") + plot_metrics_dashboard(metrics_path, out_path=out_path, show=args.show, top_n=args.top_n) + + +if __name__ == "__main__": + main()