Loosebag commited on
Commit
1b72fa2
·
0 Parent(s):

feat(traffic-rl): build adaptive traffic intelligence system

Browse files
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+
7
+ # Environments and local tooling
8
+ .venv/
9
+ .venv_bootstrap/
10
+ .virtualenv_appdata/
11
+ .tmp/
12
+
13
+ # Test/build artifacts
14
+ .pytest_cache/
15
+ .cache/
16
+
17
+ # Output artifacts
18
+ outputs/
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
PR_DESCRIPTION.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ feat(rl-traffic): Build adaptive RL traffic intelligence with baseline, DQN, and demo pipeline
2
+
3
+ Implemented a full modular RL traffic control system with deterministic OpenEnv-style simulation, dense reward engineering, fixed-time baseline benchmarking, and a production-ready DQN training/evaluation pipeline.
4
+
5
+ The changes were made to provide a complete hackathon-ready project that demonstrates measurable improvement over fixed timing while preserving interpretability and deterministic testability.
6
+
7
+ Alternative considered: integrating external simulators (e.g., SUMO), but this was intentionally avoided to keep execution fast, self-contained, and reproducible for hackathon judging constraints.
8
+
9
+ Includes emergency-priority handling, stochastic + peak/off-peak traffic modeling, bonus multi-intersection support, visualization artifacts, and reusable skill-style modules for environment/reward/training/evaluation/visualization orchestration.
10
+
11
+ Measured on the included demo run:
12
+ - waiting time improvement: 9.82%
13
+ - queue length improvement: 1.64%
14
+ - throughput gain: 7.62%
15
+ - ambulance clearances improved from 1.0 to 2.0 in emergency scenario
README.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RL-Based Adaptive Traffic Intelligence System
2
+
3
+ A modular, production-style Reinforcement Learning project for adaptive traffic signal control.
4
+
5
+ ## Problem Statement
6
+ Traffic control is treated as a sequential decision-making problem. An agent selects signal actions each step to:
7
+ - minimize waiting time
8
+ - minimize queue length
9
+ - maximize throughput
10
+ - prioritize emergency vehicles
11
+
12
+ ## Highlights
13
+ - OpenEnv-style deterministic environment (`TrafficEnv`)
14
+ - Dense multi-component reward (no sparse-only objective)
15
+ - Fixed-time baseline controller for mandatory comparison
16
+ - Lightweight PyTorch DQN (replay buffer, epsilon-greedy, target network)
17
+ - Realistic traffic dynamics (stochastic arrivals, lane imbalance, peak/off-peak)
18
+ - Emergency vehicle priority handling
19
+ - Bonus multi-intersection decentralized simulator
20
+ - Visualization + end-to-end demo pipeline
21
+ - Skill-style modular wrappers for orchestration
22
+
23
+ ## Architecture
24
+ - `traffic_rl/env`: core environment + multi-intersection extension
25
+ - `traffic_rl/reward`: reward engineering logic
26
+ - `traffic_rl/baseline`: fixed-time control baseline
27
+ - `traffic_rl/agent`: DQN and replay buffer
28
+ - `traffic_rl/training`: training loop
29
+ - `traffic_rl/evaluation`: metrics + policy comparison
30
+ - `traffic_rl/visualization`: dashboard plots
31
+ - `traffic_rl/demo`: demo pipeline runner
32
+ - `traffic_rl/skills`: modular reusable wrappers
33
+
34
+ ## State, Action, Reward
35
+ ### State
36
+ `[Q1,Q2,Q3,Q4,W1,W2,W3,W4,phase,ambulance_flag]`
37
+
38
+ ### Action Space
39
+ - `0`: hold current phase
40
+ - `1`: switch/set NS green phase
41
+ - `2`: switch/set EW green phase
42
+
43
+ ### Reward
44
+ `R_total = R_base + flow_bonus - congestion_penalty + emergency_bonus - switch_penalty`
45
+
46
+ Where:
47
+ - `R_base = - (queue_length + waiting_time)`
48
+ - `flow_bonus` rewards throughput
49
+ - `congestion_penalty` penalizes overloaded states
50
+ - `emergency_bonus` rewards ambulance clearance and penalizes ambulance delay
51
+ - `switch_penalty` discourages unstable phase flapping
52
+
53
+ ## Quick Start
54
+ Use the project-local virtual environment.
55
+
56
+ ```powershell
57
+ # run tests
58
+ .\.venv\Scripts\python.exe -m pytest
59
+
60
+ # run demo pipeline
61
+ .\.venv\Scripts\python.exe run_demo.py --episodes 70 --eval-episodes 20 --output-dir outputs
62
+ ```
63
+
64
+ ## Demo Pipeline
65
+ The demo performs:
66
+ 1. fixed baseline simulation
67
+ 2. RL training
68
+ 3. RL evaluation
69
+ 4. emergency-priority scenario
70
+ 5. metrics + plots export
71
+
72
+ Artifacts are saved to `outputs/`:
73
+ - `metrics_summary.json`
74
+ - `training_history.png`
75
+ - `policy_comparison.png`
76
+ - `fixed_trajectory.png`
77
+ - `rl_trajectory.png`
78
+
79
+ ## Measured Results (Latest Run)
80
+ From `outputs/metrics_summary.json`:
81
+
82
+ ### Baseline vs RL
83
+ - Waiting time improvement: **9.82%**
84
+ - Queue length improvement: **1.64%**
85
+ - Throughput gain: **7.62%**
86
+
87
+ ### Emergency Scenario
88
+ - Baseline ambulance clearances: **1.0**
89
+ - RL ambulance clearances: **2.0**
90
+
91
+ ## Skill-Modular Components
92
+ Reusable orchestration wrappers:
93
+ - `env_builder_skill.py`
94
+ - `reward_engineering_skill.py`
95
+ - `baseline_skill.py`
96
+ - `dqn_training_skill.py`
97
+ - `evaluation_skill.py`
98
+ - `visualization_skill.py`
99
+
100
+ ## Test Coverage
101
+ TDD tests include:
102
+ - Environment reset/step contracts, deterministic behavior, non-negative state
103
+ - Reward monotonicity and penalty/bonus triggers
104
+ - Baseline phase alternation logic
105
+ - DQN action validity and Q-value output shape
106
+ - Training/evaluation smoke tests
107
+ - Multi-intersection step contract
pytest.ini ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [pytest]
2
+ addopts = -q -p no:cacheprovider
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ pytest
2
+ numpy
3
+ matplotlib
4
+ torch
run_demo.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from traffic_rl.demo.run_demo import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
tests/conftest.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ ROOT = Path(__file__).resolve().parents[1]
5
+ if str(ROOT) not in sys.path:
6
+ sys.path.insert(0, str(ROOT))
tests/test_agent.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+ from traffic_rl.agent.dqn_agent import DQNAgent
5
+
6
+
7
+ def test_dqn_valid_action_output():
8
+ agent = DQNAgent(state_dim=10, action_dim=3, seed=7)
9
+ state = np.zeros(10, dtype=np.float32)
10
+
11
+ action = agent.select_action(state, epsilon=0.0)
12
+
13
+ assert isinstance(action, int)
14
+ assert 0 <= action < 3
15
+
16
+
17
+ def test_dqn_q_value_shape():
18
+ agent = DQNAgent(state_dim=10, action_dim=3, seed=7)
19
+ batch = torch.zeros((4, 10), dtype=torch.float32)
20
+
21
+ q_values = agent.q_network(batch)
22
+
23
+ assert tuple(q_values.shape) == (4, 3)
tests/test_baseline.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from traffic_rl.baseline.fixed_time_controller import FixedTimeController
2
+
3
+
4
+ def test_fixed_signal_alternates_correctly():
5
+ controller = FixedTimeController(switch_interval=2)
6
+ phases = [controller.phase_for_step(step) for step in range(8)]
7
+
8
+ assert phases == [0, 0, 1, 1, 0, 0, 1, 1]
tests/test_env.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from traffic_rl.env.traffic_env import TrafficEnv
4
+
5
+
6
+ def make_env(**overrides):
7
+ config = {
8
+ "max_steps": 20,
9
+ "arrival_mode": "deterministic",
10
+ "arrival_sequence": [
11
+ [2, 1, 0, 0],
12
+ [1, 0, 2, 0],
13
+ [0, 1, 1, 0],
14
+ [1, 1, 0, 2],
15
+ ],
16
+ "service_rate": 2,
17
+ "seed": 123,
18
+ }
19
+ config.update(overrides)
20
+ return TrafficEnv(config=config)
21
+
22
+
23
+ def test_reset_returns_valid_state():
24
+ env = make_env()
25
+ state = env.reset()
26
+
27
+ assert isinstance(state, np.ndarray)
28
+ assert state.shape == (10,)
29
+ assert np.all(state[:8] >= 0)
30
+ assert state[8] in (0, 1)
31
+
32
+
33
+ def test_step_updates_queues_and_non_negative():
34
+ env = make_env(arrival_sequence=[[1, 1, 0, 0]], service_rate=1)
35
+ env.reset()
36
+
37
+ next_state, reward, done, info = env.step(1)
38
+
39
+ assert next_state.shape == (10,)
40
+ assert np.all(next_state[:8] >= 0)
41
+ assert isinstance(reward, float)
42
+ assert isinstance(done, bool)
43
+ assert "throughput" in info
44
+
45
+
46
+ def test_environment_deterministic_transitions():
47
+ env1 = make_env()
48
+ env2 = make_env()
49
+
50
+ s1 = env1.reset()
51
+ s2 = env2.reset()
52
+ assert np.allclose(s1, s2)
53
+
54
+ actions = [1, 0, 2, 0, 1]
55
+ for a in actions:
56
+ ns1, r1, d1, i1 = env1.step(a)
57
+ ns2, r2, d2, i2 = env2.step(a)
58
+ assert np.allclose(ns1, ns2)
59
+ assert r1 == r2
60
+ assert d1 == d2
61
+ assert i1["throughput"] == i2["throughput"]
62
+
63
+
64
+ def test_no_negative_values_over_rollout():
65
+ env = make_env()
66
+ env.reset()
67
+ for _ in range(10):
68
+ state, _, done, _ = env.step(0)
69
+ assert np.all(state[:8] >= 0)
70
+ if done:
71
+ break
tests/test_multi_agent.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from traffic_rl.env.multi_intersection_env import MultiIntersectionEnv
2
+
3
+
4
+ def test_multi_intersection_step_contract():
5
+ env = MultiIntersectionEnv(num_intersections=2, env_config={"max_steps": 10, "ambulance_spawn_prob": 0.0})
6
+
7
+ states = env.reset()
8
+ assert len(states) == 2
9
+ assert states[0].shape == (10,)
10
+
11
+ next_states, rewards, dones, infos = env.step([1, 2])
12
+ assert len(next_states) == 2
13
+ assert len(rewards) == 2
14
+ assert len(dones) == 2
15
+ assert isinstance(infos[0], dict)
tests/test_reward.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from traffic_rl.reward.reward_engine import RewardEngine
2
+
3
+
4
+ def test_higher_congestion_yields_lower_reward():
5
+ reward_engine = RewardEngine()
6
+
7
+ low = reward_engine.compute(
8
+ queue_sum=5,
9
+ waiting_sum=8,
10
+ throughput=4,
11
+ switched=False,
12
+ ambulance_wait=0,
13
+ ambulance_cleared=False,
14
+ )
15
+ high = reward_engine.compute(
16
+ queue_sum=20,
17
+ waiting_sum=30,
18
+ throughput=4,
19
+ switched=False,
20
+ ambulance_wait=0,
21
+ ambulance_cleared=False,
22
+ )
23
+
24
+ assert high < low
25
+
26
+
27
+ def test_ambulance_priority_bonus_applies():
28
+ reward_engine = RewardEngine()
29
+
30
+ no_clear = reward_engine.compute(
31
+ queue_sum=10,
32
+ waiting_sum=10,
33
+ throughput=2,
34
+ switched=False,
35
+ ambulance_wait=3,
36
+ ambulance_cleared=False,
37
+ )
38
+ cleared = reward_engine.compute(
39
+ queue_sum=10,
40
+ waiting_sum=10,
41
+ throughput=2,
42
+ switched=False,
43
+ ambulance_wait=0,
44
+ ambulance_cleared=True,
45
+ )
46
+
47
+ assert cleared > no_clear
48
+
49
+
50
+ def test_switch_penalty_triggers():
51
+ reward_engine = RewardEngine()
52
+
53
+ keep_phase = reward_engine.compute(
54
+ queue_sum=8,
55
+ waiting_sum=8,
56
+ throughput=3,
57
+ switched=False,
58
+ ambulance_wait=0,
59
+ ambulance_cleared=False,
60
+ )
61
+ switched = reward_engine.compute(
62
+ queue_sum=8,
63
+ waiting_sum=8,
64
+ throughput=3,
65
+ switched=True,
66
+ ambulance_wait=0,
67
+ ambulance_cleared=False,
68
+ )
69
+
70
+ assert switched < keep_phase
tests/test_training_eval.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from traffic_rl.env.traffic_env import TrafficEnv
2
+ from traffic_rl.training.trainer import TrainingConfig, train_dqn
3
+ from traffic_rl.evaluation.evaluator import evaluate_agent, evaluate_fixed_controller
4
+
5
+
6
+ def test_training_loop_returns_history():
7
+ env = TrafficEnv(
8
+ config={
9
+ "max_steps": 20,
10
+ "arrival_mode": "deterministic",
11
+ "arrival_sequence": [[1, 1, 1, 1]],
12
+ "ambulance_spawn_prob": 0.0,
13
+ "seed": 11,
14
+ }
15
+ )
16
+ cfg = TrainingConfig(episodes=4, max_steps=20, batch_size=8, target_sync_interval=2)
17
+
18
+ agent, history = train_dqn(env=env, config=cfg)
19
+
20
+ assert len(history["episode_reward"]) == 4
21
+ assert len(history["avg_queue"]) == 4
22
+ assert agent.action_dim == 3
23
+
24
+
25
+ def test_evaluation_returns_metrics():
26
+ env_config = {
27
+ "max_steps": 20,
28
+ "arrival_mode": "deterministic",
29
+ "arrival_sequence": [[2, 1, 2, 1]],
30
+ "ambulance_spawn_prob": 0.0,
31
+ "seed": 12,
32
+ }
33
+ env = TrafficEnv(config=env_config)
34
+ cfg = TrainingConfig(episodes=3, max_steps=20, batch_size=8, target_sync_interval=2)
35
+ agent, _ = train_dqn(env=env, config=cfg)
36
+
37
+ fixed_metrics = evaluate_fixed_controller(env_config=env_config, episodes=2, switch_interval=2)
38
+ rl_metrics = evaluate_agent(agent=agent, env_config=env_config, episodes=2)
39
+
40
+ assert fixed_metrics["avg_waiting_time"] >= 0
41
+ assert rl_metrics["avg_queue_length"] >= 0
42
+ assert "throughput" in fixed_metrics
43
+ assert "throughput" in rl_metrics
traffic_rl/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """RL-Based Adaptive Traffic Intelligence System."""
2
+
traffic_rl/agent/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """DQN agent package."""
2
+
3
+ from .dqn_agent import DQNAgent
4
+
5
+ __all__ = ["DQNAgent"]
traffic_rl/agent/dqn_agent.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ import numpy as np
6
+ import torch
7
+ from torch import nn
8
+
9
+ from .replay_buffer import ReplayBuffer
10
+
11
+
12
+ class QNetwork(nn.Module):
13
+ def __init__(self, state_dim: int, action_dim: int) -> None:
14
+ super().__init__()
15
+ self.model = nn.Sequential(
16
+ nn.Linear(state_dim, 64),
17
+ nn.ReLU(),
18
+ nn.Linear(64, 64),
19
+ nn.ReLU(),
20
+ nn.Linear(64, action_dim),
21
+ )
22
+
23
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
24
+ return self.model(x)
25
+
26
+
27
+ class DQNAgent:
28
+ def __init__(
29
+ self,
30
+ state_dim: int,
31
+ action_dim: int,
32
+ learning_rate: float = 1e-3,
33
+ gamma: float = 0.99,
34
+ buffer_size: int = 50000,
35
+ batch_size: int = 64,
36
+ seed: int = 42,
37
+ ) -> None:
38
+ self.state_dim = state_dim
39
+ self.action_dim = action_dim
40
+ self.gamma = gamma
41
+ self.batch_size = batch_size
42
+
43
+ random.seed(seed)
44
+ np.random.seed(seed)
45
+ torch.manual_seed(seed)
46
+
47
+ self.q_network = QNetwork(state_dim, action_dim)
48
+ self.target_network = QNetwork(state_dim, action_dim)
49
+ self.target_network.load_state_dict(self.q_network.state_dict())
50
+ self.target_network.eval()
51
+
52
+ self.optimizer = torch.optim.Adam(self.q_network.parameters(), lr=learning_rate)
53
+ self.loss_fn = nn.MSELoss()
54
+ self.replay_buffer = ReplayBuffer(capacity=buffer_size, seed=seed)
55
+
56
+ def select_action(self, state: np.ndarray, epsilon: float = 0.1) -> int:
57
+ if random.random() < epsilon:
58
+ return random.randrange(self.action_dim)
59
+
60
+ state_tensor = torch.as_tensor(state, dtype=torch.float32).unsqueeze(0)
61
+ with torch.no_grad():
62
+ q_values = self.q_network(state_tensor)
63
+ return int(torch.argmax(q_values, dim=1).item())
64
+
65
+ def store_transition(
66
+ self,
67
+ state: np.ndarray,
68
+ action: int,
69
+ reward: float,
70
+ next_state: np.ndarray,
71
+ done: bool,
72
+ ) -> None:
73
+ self.replay_buffer.add(state, action, reward, next_state, done)
74
+
75
+ def update(self) -> float | None:
76
+ if len(self.replay_buffer) < self.batch_size:
77
+ return None
78
+
79
+ transitions = self.replay_buffer.sample(self.batch_size)
80
+
81
+ states = torch.as_tensor(np.stack([t.state for t in transitions]), dtype=torch.float32)
82
+ actions = torch.as_tensor([t.action for t in transitions], dtype=torch.int64).unsqueeze(1)
83
+ rewards = torch.as_tensor([t.reward for t in transitions], dtype=torch.float32).unsqueeze(1)
84
+ next_states = torch.as_tensor(np.stack([t.next_state for t in transitions]), dtype=torch.float32)
85
+ dones = torch.as_tensor([t.done for t in transitions], dtype=torch.float32).unsqueeze(1)
86
+
87
+ q_values = self.q_network(states).gather(1, actions)
88
+
89
+ with torch.no_grad():
90
+ next_q = self.target_network(next_states).max(dim=1, keepdim=True)[0]
91
+ targets = rewards + (1.0 - dones) * self.gamma * next_q
92
+
93
+ loss = self.loss_fn(q_values, targets)
94
+
95
+ self.optimizer.zero_grad()
96
+ loss.backward()
97
+ self.optimizer.step()
98
+
99
+ return float(loss.item())
100
+
101
+ def sync_target(self) -> None:
102
+ self.target_network.load_state_dict(self.q_network.state_dict())
traffic_rl/agent/replay_buffer.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from collections import deque
5
+ from dataclasses import dataclass
6
+ from typing import Deque
7
+
8
+ import numpy as np
9
+
10
+
11
+ @dataclass
12
+ class Transition:
13
+ state: np.ndarray
14
+ action: int
15
+ reward: float
16
+ next_state: np.ndarray
17
+ done: float
18
+
19
+
20
+ class ReplayBuffer:
21
+ def __init__(self, capacity: int = 10000, seed: int | None = None) -> None:
22
+ if capacity <= 0:
23
+ raise ValueError("capacity must be > 0")
24
+ self.capacity = capacity
25
+ self.buffer: Deque[Transition] = deque(maxlen=capacity)
26
+ self._random = random.Random(seed)
27
+
28
+ def __len__(self) -> int:
29
+ return len(self.buffer)
30
+
31
+ def add(self, state: np.ndarray, action: int, reward: float, next_state: np.ndarray, done: bool) -> None:
32
+ self.buffer.append(
33
+ Transition(
34
+ state=np.asarray(state, dtype=np.float32),
35
+ action=int(action),
36
+ reward=float(reward),
37
+ next_state=np.asarray(next_state, dtype=np.float32),
38
+ done=float(done),
39
+ )
40
+ )
41
+
42
+ def sample(self, batch_size: int) -> list[Transition]:
43
+ return self._random.sample(list(self.buffer), batch_size)
traffic_rl/baseline/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Baseline controllers."""
2
+
3
+ from .fixed_time_controller import FixedTimeController
4
+
5
+ __all__ = ["FixedTimeController"]
traffic_rl/baseline/fixed_time_controller.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ class FixedTimeController:
5
+ """Simple fixed-time phase alternator used as benchmark."""
6
+
7
+ def __init__(self, switch_interval: int = 5) -> None:
8
+ if switch_interval <= 0:
9
+ raise ValueError("switch_interval must be positive")
10
+ self.switch_interval = switch_interval
11
+
12
+ def phase_for_step(self, step: int) -> int:
13
+ if step < 0:
14
+ raise ValueError("step must be non-negative")
15
+ return (step // self.switch_interval) % 2
16
+
17
+ def action_for_step(self, step: int) -> int:
18
+ phase = self.phase_for_step(step)
19
+ return 1 if phase == 0 else 2
traffic_rl/demo/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Demo pipeline package."""
traffic_rl/demo/run_demo.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from traffic_rl.baseline.fixed_time_controller import FixedTimeController
8
+ from traffic_rl.env.traffic_env import TrafficEnv
9
+ from traffic_rl.evaluation.evaluator import (
10
+ collect_trajectory,
11
+ compare_policies,
12
+ evaluate_agent,
13
+ evaluate_fixed_controller,
14
+ )
15
+ from traffic_rl.training.trainer import TrainingConfig, train_dqn
16
+ from traffic_rl.visualization.dashboard import plot_comparison, plot_training_history, plot_trajectory
17
+
18
+
19
+ def _print_metrics(title: str, metrics: dict[str, float]) -> None:
20
+ print(f"\n{title}")
21
+ print("-" * len(title))
22
+ for key, value in metrics.items():
23
+ print(f"{key}: {value:.3f}")
24
+
25
+
26
+ def main() -> None:
27
+ parser = argparse.ArgumentParser(description="RL-Based Adaptive Traffic Intelligence Demo")
28
+ parser.add_argument("--episodes", type=int, default=80)
29
+ parser.add_argument("--eval-episodes", type=int, default=20)
30
+ parser.add_argument("--output-dir", type=str, default="outputs")
31
+ args = parser.parse_args()
32
+
33
+ output_dir = Path(args.output_dir)
34
+ output_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ env_config = {
37
+ "max_steps": 120,
38
+ "arrival_mode": "stochastic",
39
+ "lane_bias": (1.6, 0.8, 1.4, 0.6),
40
+ "peak_rates": (3.8, 2.2, 3.4, 1.5),
41
+ "offpeak_rates": (1.4, 0.9, 1.2, 0.7),
42
+ "peak_duration": 35,
43
+ "cycle_duration": 60,
44
+ "service_rate": 2,
45
+ "ambulance_spawn_prob": 0.08,
46
+ "seed": 42,
47
+ }
48
+
49
+ print("1) Running fixed-time baseline...")
50
+ baseline_metrics = evaluate_fixed_controller(
51
+ env_config=env_config,
52
+ episodes=args.eval_episodes,
53
+ switch_interval=5,
54
+ )
55
+
56
+ print("2) Training RL (DQN) agent...")
57
+ train_env = TrafficEnv(config=env_config)
58
+ training_config = TrainingConfig(
59
+ episodes=args.episodes,
60
+ max_steps=env_config["max_steps"],
61
+ batch_size=64,
62
+ target_sync_interval=10,
63
+ epsilon_decay=0.97,
64
+ )
65
+ agent, history = train_dqn(train_env, training_config)
66
+
67
+ print("3) Evaluating RL agent...")
68
+ rl_metrics = evaluate_agent(agent=agent, env_config=env_config, episodes=args.eval_episodes)
69
+ comparison = compare_policies(baseline_metrics, rl_metrics)
70
+
71
+ ambulance_env = {
72
+ **env_config,
73
+ "ambulance_spawn_prob": 0.25,
74
+ "seed": 99,
75
+ }
76
+ baseline_amb = evaluate_fixed_controller(env_config=ambulance_env, episodes=10, switch_interval=5)
77
+ rl_amb = evaluate_agent(agent=agent, env_config=ambulance_env, episodes=10)
78
+
79
+ _print_metrics("Fixed-Time Baseline", baseline_metrics)
80
+ _print_metrics("RL Agent", rl_metrics)
81
+ _print_metrics("Improvement (RL vs Fixed)", comparison)
82
+
83
+ print("\n4) Ambulance Priority Scenario")
84
+ print(f"fixed ambulance_clearances: {baseline_amb['ambulance_clearances']:.3f}")
85
+ print(f"rl ambulance_clearances: {rl_amb['ambulance_clearances']:.3f}")
86
+
87
+ print("\n5) Generating visualizations...")
88
+ saved = {}
89
+ saved.update(plot_training_history(history, output_dir=output_dir))
90
+ saved.update(plot_comparison(baseline_metrics, rl_metrics, output_dir=output_dir))
91
+
92
+ fixed_controller = FixedTimeController(switch_interval=5)
93
+ fixed_trace = collect_trajectory(TrafficEnv(config=env_config), lambda _s, step: fixed_controller.action_for_step(step))
94
+ rl_trace = collect_trajectory(TrafficEnv(config=env_config), lambda state, _step: agent.select_action(state, epsilon=0.0))
95
+
96
+ saved.update(plot_trajectory(fixed_trace, output_dir=output_dir, name="fixed_trajectory"))
97
+ saved.update(plot_trajectory(rl_trace, output_dir=output_dir, name="rl_trajectory"))
98
+
99
+ summary = {
100
+ "baseline": baseline_metrics,
101
+ "rl": rl_metrics,
102
+ "comparison": comparison,
103
+ "ambulance_baseline": baseline_amb,
104
+ "ambulance_rl": rl_amb,
105
+ "artifacts": saved,
106
+ }
107
+
108
+ summary_path = output_dir / "metrics_summary.json"
109
+ summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
110
+
111
+ print("\nDemo completed.")
112
+ print(f"Metrics summary: {summary_path}")
113
+ for name, path in saved.items():
114
+ print(f"{name}: {path}")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
traffic_rl/env/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Environment package."""
2
+
3
+ from .multi_intersection_env import MultiIntersectionEnv
4
+ from .traffic_env import TrafficEnv
5
+
6
+ __all__ = ["TrafficEnv", "MultiIntersectionEnv"]
traffic_rl/env/multi_intersection_env.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+
7
+ from traffic_rl.env.traffic_env import TrafficEnv
8
+
9
+
10
+ @dataclass
11
+ class MultiStepResult:
12
+ states: list[np.ndarray]
13
+ rewards: list[float]
14
+ dones: list[bool]
15
+ infos: list[dict]
16
+
17
+
18
+ class MultiIntersectionEnv:
19
+ """Bonus decentralized multi-intersection simulator."""
20
+
21
+ def __init__(
22
+ self,
23
+ num_intersections: int = 2,
24
+ env_config: dict | None = None,
25
+ transfer_ratio: float = 0.2,
26
+ ) -> None:
27
+ if num_intersections <= 0:
28
+ raise ValueError("num_intersections must be > 0")
29
+
30
+ self.transfer_ratio = transfer_ratio
31
+ self.intersections = [TrafficEnv(config=env_config or {}) for _ in range(num_intersections)]
32
+
33
+ def reset(self) -> list[np.ndarray]:
34
+ return [env.reset() for env in self.intersections]
35
+
36
+ def step(self, actions: list[int]) -> tuple[list[np.ndarray], list[float], list[bool], list[dict]]:
37
+ if len(actions) != len(self.intersections):
38
+ raise ValueError("actions length must match number of intersections")
39
+
40
+ states = []
41
+ rewards = []
42
+ dones = []
43
+ infos = []
44
+
45
+ upstream_flow = 0
46
+ for idx, (env, action) in enumerate(zip(self.intersections, actions)):
47
+ if idx > 0 and upstream_flow > 0:
48
+ extra = int(round(upstream_flow * self.transfer_ratio))
49
+ env._apply_arrivals([extra, 0, 0, 0])
50
+
51
+ state, reward, done, info = env.step(action)
52
+ states.append(state)
53
+ rewards.append(float(reward))
54
+ dones.append(bool(done))
55
+ infos.append(info)
56
+ upstream_flow = int(info.get("throughput", 0))
57
+
58
+ return states, rewards, dones, infos
traffic_rl/env/traffic_env.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+
6
+ import numpy as np
7
+
8
+ from traffic_rl.reward.reward_engine import RewardEngine
9
+
10
+
11
+ @dataclass
12
+ class EnvConfig:
13
+ max_steps: int = 200
14
+ service_rate: int = 2
15
+ arrival_mode: str = "stochastic"
16
+ arrival_sequence: list[list[int]] | None = None
17
+ seed: int = 42
18
+ ambulance_spawn_prob: float = 0.05
19
+ ambulance_lane: int = 0
20
+ lane_bias: tuple[float, float, float, float] = (1.3, 0.8, 1.1, 0.6)
21
+ peak_rates: tuple[float, float, float, float] = (3.2, 2.0, 2.8, 1.4)
22
+ offpeak_rates: tuple[float, float, float, float] = (1.2, 0.8, 1.0, 0.6)
23
+ peak_duration: int = 20
24
+ cycle_duration: int = 40
25
+
26
+
27
+ class TrafficEnv:
28
+ """Deterministic and fast OpenEnv-style traffic control environment."""
29
+
30
+ def __init__(self, config: dict | None = None, reward_engine: RewardEngine | None = None) -> None:
31
+ raw = config or {}
32
+ self.config = EnvConfig(**{**EnvConfig().__dict__, **raw})
33
+ self.reward_engine = reward_engine or RewardEngine()
34
+
35
+ self.rng = np.random.default_rng(self.config.seed)
36
+ self.step_count = 0
37
+ self.phase = 0 # 0: NS green, 1: EW green
38
+ self.lane_queues: list[deque[tuple[str, int]]] = [deque() for _ in range(4)]
39
+ self.ambulance_active = False
40
+
41
+ def reset(self) -> np.ndarray:
42
+ self.step_count = 0
43
+ self.phase = 0
44
+ self.lane_queues = [deque() for _ in range(4)]
45
+ self.ambulance_active = False
46
+ return self.observation()
47
+
48
+ def observation(self) -> np.ndarray:
49
+ queue_lengths = [len(q) for q in self.lane_queues]
50
+ waiting_loads = [sum(wait for _, wait in q) for q in self.lane_queues]
51
+ ambulance_flag = 1.0 if self.ambulance_active else 0.0
52
+ obs = np.asarray(
53
+ queue_lengths + waiting_loads + [float(self.phase), ambulance_flag],
54
+ dtype=np.float32,
55
+ )
56
+ return obs
57
+
58
+ def compute_reward(
59
+ self,
60
+ *,
61
+ throughput: int,
62
+ switched: bool,
63
+ ambulance_cleared: bool,
64
+ ) -> float:
65
+ queue_sum = float(sum(len(q) for q in self.lane_queues))
66
+ waiting_sum = float(sum(sum(wait for _, wait in q) for q in self.lane_queues))
67
+ ambulance_wait = float(self._ambulance_wait())
68
+ return self.reward_engine.compute(
69
+ queue_sum=queue_sum,
70
+ waiting_sum=waiting_sum,
71
+ throughput=float(throughput),
72
+ switched=switched,
73
+ ambulance_wait=ambulance_wait,
74
+ ambulance_cleared=ambulance_cleared,
75
+ )
76
+
77
+ def step(self, action: int) -> tuple[np.ndarray, float, bool, dict]:
78
+ if action not in (0, 1, 2):
79
+ raise ValueError("action must be 0 (hold), 1 (NS), or 2 (EW)")
80
+
81
+ previous_phase = self.phase
82
+ if action == 1:
83
+ self.phase = 0
84
+ elif action == 2:
85
+ self.phase = 1
86
+ switched = self.phase != previous_phase
87
+
88
+ arrivals = self._next_arrivals()
89
+ self._apply_arrivals(arrivals)
90
+ self._maybe_spawn_ambulance()
91
+
92
+ throughput, ambulance_cleared = self._serve_current_phase()
93
+ self._increment_wait_times()
94
+
95
+ reward = self.compute_reward(
96
+ throughput=throughput,
97
+ switched=switched,
98
+ ambulance_cleared=ambulance_cleared,
99
+ )
100
+
101
+ self.step_count += 1
102
+ done = self.step_count >= self.config.max_steps
103
+
104
+ obs = self.observation()
105
+ info = {
106
+ "throughput": throughput,
107
+ "queue_sum": int(sum(obs[:4])),
108
+ "waiting_sum": float(np.sum(obs[4:8])),
109
+ "phase": int(self.phase),
110
+ "switched": switched,
111
+ "ambulance_cleared": ambulance_cleared,
112
+ "arrivals": arrivals,
113
+ }
114
+ return obs, float(reward), bool(done), info
115
+
116
+ def _next_arrivals(self) -> list[int]:
117
+ if self.config.arrival_mode == "deterministic" and self.config.arrival_sequence:
118
+ idx = self.step_count % len(self.config.arrival_sequence)
119
+ vals = self.config.arrival_sequence[idx]
120
+ return [max(0, int(v)) for v in vals]
121
+
122
+ phase_pos = self.step_count % self.config.cycle_duration
123
+ is_peak = phase_pos < self.config.peak_duration
124
+ base = self.config.peak_rates if is_peak else self.config.offpeak_rates
125
+ rates = np.asarray(base, dtype=np.float32) * np.asarray(self.config.lane_bias, dtype=np.float32)
126
+ arrivals = self.rng.poisson(rates).astype(int).tolist()
127
+ return [max(0, int(v)) for v in arrivals]
128
+
129
+ def _apply_arrivals(self, arrivals: list[int]) -> None:
130
+ for lane_idx, count in enumerate(arrivals):
131
+ for _ in range(max(0, count)):
132
+ self.lane_queues[lane_idx].append(("car", 0))
133
+
134
+ def _maybe_spawn_ambulance(self) -> None:
135
+ if self.ambulance_active:
136
+ return
137
+
138
+ if self.rng.random() < self.config.ambulance_spawn_prob:
139
+ lane = int(self.config.ambulance_lane)
140
+ self.lane_queues[lane].append(("ambulance", 0))
141
+ self.ambulance_active = True
142
+
143
+ def _serve_current_phase(self) -> tuple[int, bool]:
144
+ green_lanes = (0, 2) if self.phase == 0 else (1, 3)
145
+ throughput = 0
146
+ ambulance_cleared = False
147
+
148
+ for lane in green_lanes:
149
+ for _ in range(self.config.service_rate):
150
+ if not self.lane_queues[lane]:
151
+ break
152
+ vehicle_type, _wait = self.lane_queues[lane].popleft()
153
+ throughput += 1
154
+ if vehicle_type == "ambulance":
155
+ ambulance_cleared = True
156
+ self.ambulance_active = False
157
+
158
+ return throughput, ambulance_cleared
159
+
160
+ def _increment_wait_times(self) -> None:
161
+ for lane_idx, lane_q in enumerate(self.lane_queues):
162
+ updated = deque((vehicle_type, wait + 1) for vehicle_type, wait in lane_q)
163
+ self.lane_queues[lane_idx] = updated
164
+
165
+ def _ambulance_wait(self) -> int:
166
+ if not self.ambulance_active:
167
+ return 0
168
+ for lane in self.lane_queues:
169
+ for vehicle_type, wait in lane:
170
+ if vehicle_type == "ambulance":
171
+ return wait
172
+ return 0
traffic_rl/evaluation/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluation and policy comparison utilities."""
2
+
3
+ from .evaluator import (
4
+ collect_trajectory,
5
+ compare_policies,
6
+ evaluate_agent,
7
+ evaluate_fixed_controller,
8
+ )
9
+
10
+ __all__ = [
11
+ "collect_trajectory",
12
+ "compare_policies",
13
+ "evaluate_agent",
14
+ "evaluate_fixed_controller",
15
+ ]
traffic_rl/evaluation/evaluator.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from statistics import mean
4
+ from typing import Callable
5
+
6
+ from traffic_rl.agent.dqn_agent import DQNAgent
7
+ from traffic_rl.baseline.fixed_time_controller import FixedTimeController
8
+ from traffic_rl.env.traffic_env import TrafficEnv
9
+
10
+
11
+ PolicyFn = Callable[[list[float], int], int]
12
+
13
+
14
+ def run_episode(env: TrafficEnv, policy: PolicyFn) -> dict[str, float]:
15
+ state = env.reset()
16
+ done = False
17
+ step_idx = 0
18
+
19
+ rewards = []
20
+ queues = []
21
+ waits = []
22
+ throughputs = []
23
+ ambulance_cleared_count = 0
24
+
25
+ while not done:
26
+ action = policy(state.tolist(), step_idx)
27
+ state, reward, done, info = env.step(action)
28
+
29
+ rewards.append(float(reward))
30
+ queues.append(float(info["queue_sum"]))
31
+ waits.append(float(info["waiting_sum"]))
32
+ throughputs.append(float(info["throughput"]))
33
+ ambulance_cleared_count += int(bool(info["ambulance_cleared"]))
34
+ step_idx += 1
35
+
36
+ return {
37
+ "reward": float(sum(rewards)),
38
+ "avg_queue_length": float(mean(queues) if queues else 0.0),
39
+ "avg_waiting_time": float(mean(waits) if waits else 0.0),
40
+ "throughput": float(sum(throughputs)),
41
+ "ambulance_clearances": float(ambulance_cleared_count),
42
+ }
43
+
44
+
45
+ def evaluate_fixed_controller(
46
+ *,
47
+ env_config: dict,
48
+ episodes: int = 20,
49
+ switch_interval: int = 5,
50
+ ) -> dict[str, float]:
51
+ controller = FixedTimeController(switch_interval=switch_interval)
52
+
53
+ def policy(_state: list[float], step: int) -> int:
54
+ return controller.action_for_step(step)
55
+
56
+ episode_metrics = []
57
+ for _ in range(episodes):
58
+ env = TrafficEnv(config=env_config)
59
+ episode_metrics.append(run_episode(env, policy))
60
+
61
+ return _aggregate(episode_metrics)
62
+
63
+
64
+ def evaluate_agent(
65
+ *,
66
+ agent: DQNAgent,
67
+ env_config: dict,
68
+ episodes: int = 20,
69
+ ) -> dict[str, float]:
70
+ def policy(state: list[float], _step: int) -> int:
71
+ return agent.select_action(state, epsilon=0.0)
72
+
73
+ episode_metrics = []
74
+ for _ in range(episodes):
75
+ env = TrafficEnv(config=env_config)
76
+ episode_metrics.append(run_episode(env, policy))
77
+
78
+ return _aggregate(episode_metrics)
79
+
80
+
81
+ def _aggregate(episode_metrics: list[dict[str, float]]) -> dict[str, float]:
82
+ keys = episode_metrics[0].keys()
83
+ return {k: float(mean(m[k] for m in episode_metrics)) for k in keys}
84
+
85
+
86
+ def compare_policies(baseline: dict[str, float], rl: dict[str, float]) -> dict[str, float]:
87
+ def pct_improve(lower_is_better_key: str) -> float:
88
+ b = baseline[lower_is_better_key]
89
+ r = rl[lower_is_better_key]
90
+ if b == 0:
91
+ return 0.0
92
+ return float(((b - r) / b) * 100.0)
93
+
94
+ def pct_gain(higher_is_better_key: str) -> float:
95
+ b = baseline[higher_is_better_key]
96
+ r = rl[higher_is_better_key]
97
+ if b == 0:
98
+ return 0.0
99
+ return float(((r - b) / b) * 100.0)
100
+
101
+ return {
102
+ "waiting_time_improvement_pct": pct_improve("avg_waiting_time"),
103
+ "queue_length_improvement_pct": pct_improve("avg_queue_length"),
104
+ "throughput_gain_pct": pct_gain("throughput"),
105
+ "ambulance_clearance_gain_pct": pct_gain("ambulance_clearances"),
106
+ }
107
+
108
+
109
+ def collect_trajectory(env: TrafficEnv, policy: PolicyFn) -> dict[str, list[float]]:
110
+ state = env.reset()
111
+ done = False
112
+ step_idx = 0
113
+
114
+ trace = {
115
+ "reward": [],
116
+ "queue_sum": [],
117
+ "waiting_sum": [],
118
+ "phase": [],
119
+ }
120
+
121
+ while not done:
122
+ action = policy(state.tolist(), step_idx)
123
+ state, reward, done, info = env.step(action)
124
+ trace["reward"].append(float(reward))
125
+ trace["queue_sum"].append(float(info["queue_sum"]))
126
+ trace["waiting_sum"].append(float(info["waiting_sum"]))
127
+ trace["phase"].append(float(info["phase"]))
128
+ step_idx += 1
129
+
130
+ return trace
traffic_rl/reward/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Reward system package."""
2
+
3
+ from .reward_engine import RewardEngine, RewardWeights
4
+
5
+ __all__ = ["RewardEngine", "RewardWeights"]
traffic_rl/reward/reward_engine.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class RewardWeights:
8
+ queue_weight: float = 1.0
9
+ waiting_weight: float = 0.35
10
+ flow_weight: float = 2.0
11
+ congestion_weight: float = 0.5
12
+ emergency_bonus: float = 40.0
13
+ ambulance_wait_penalty: float = 3.0
14
+ switch_penalty: float = 1.5
15
+ congestion_threshold: float = 25.0
16
+
17
+
18
+ class RewardEngine:
19
+ """Dense multi-objective reward for adaptive signal control."""
20
+
21
+ def __init__(self, weights: RewardWeights | None = None) -> None:
22
+ self.weights = weights or RewardWeights()
23
+
24
+ def compute(
25
+ self,
26
+ *,
27
+ queue_sum: float,
28
+ waiting_sum: float,
29
+ throughput: float,
30
+ switched: bool,
31
+ ambulance_wait: float,
32
+ ambulance_cleared: bool,
33
+ ) -> float:
34
+ w = self.weights
35
+
36
+ base_cost = (w.queue_weight * queue_sum) + (w.waiting_weight * waiting_sum)
37
+ base_reward = -base_cost
38
+
39
+ flow_bonus = w.flow_weight * throughput
40
+ congestion_penalty = w.congestion_weight * max(queue_sum - w.congestion_threshold, 0.0)
41
+
42
+ emergency_term = -w.ambulance_wait_penalty * ambulance_wait
43
+ if ambulance_cleared:
44
+ emergency_term += w.emergency_bonus
45
+
46
+ switch_term = -w.switch_penalty if switched else 0.0
47
+
48
+ total_reward = base_reward + flow_bonus - congestion_penalty + emergency_term + switch_term
49
+ return float(total_reward)
traffic_rl/skills/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Skill wrappers for modular orchestration."""
traffic_rl/skills/baseline_skill.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from traffic_rl.evaluation.evaluator import evaluate_fixed_controller
4
+
5
+
6
+ def run_fixed_baseline(env_config: dict, episodes: int = 20, switch_interval: int = 5) -> dict[str, float]:
7
+ return evaluate_fixed_controller(env_config=env_config, episodes=episodes, switch_interval=switch_interval)
traffic_rl/skills/dqn_training_skill.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from traffic_rl.env.traffic_env import TrafficEnv
4
+ from traffic_rl.training.trainer import TrainingConfig, train_dqn
5
+
6
+
7
+ def train_agent(env_config: dict, config: TrainingConfig | None = None):
8
+ env = TrafficEnv(config=env_config)
9
+ return train_dqn(env=env, config=config)
traffic_rl/skills/env_builder_skill.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from traffic_rl.env.traffic_env import TrafficEnv
4
+
5
+
6
+ def build_environment(config: dict | None = None) -> TrafficEnv:
7
+ return TrafficEnv(config=config or {})
traffic_rl/skills/evaluation_skill.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from traffic_rl.evaluation.evaluator import compare_policies, evaluate_agent
4
+
5
+
6
+ def evaluate_system(agent, env_config: dict, baseline_metrics: dict, episodes: int = 20) -> dict:
7
+ rl_metrics = evaluate_agent(agent=agent, env_config=env_config, episodes=episodes)
8
+ comparison = compare_policies(baseline_metrics, rl_metrics)
9
+ return {"rl": rl_metrics, "comparison": comparison}
traffic_rl/skills/reward_engineering_skill.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from traffic_rl.reward.reward_engine import RewardEngine, RewardWeights
4
+
5
+
6
+ def build_reward_engine(weights: dict | None = None) -> RewardEngine:
7
+ if not weights:
8
+ return RewardEngine()
9
+ return RewardEngine(weights=RewardWeights(**weights))
traffic_rl/skills/visualization_skill.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from traffic_rl.visualization.dashboard import plot_comparison, plot_training_history, plot_trajectory
4
+
5
+
6
+ def render_dashboard(history: dict, baseline: dict, rl: dict, fixed_trace: dict, rl_trace: dict, output_dir: str = "outputs") -> dict[str, str]:
7
+ saved = {}
8
+ saved.update(plot_training_history(history, output_dir=output_dir))
9
+ saved.update(plot_comparison(baseline, rl, output_dir=output_dir))
10
+ saved.update(plot_trajectory(fixed_trace, output_dir=output_dir, name="fixed_trajectory"))
11
+ saved.update(plot_trajectory(rl_trace, output_dir=output_dir, name="rl_trajectory"))
12
+ return saved
traffic_rl/training/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Training workflows for RL traffic control."""
2
+
3
+ from .trainer import TrainingConfig, train_dqn
4
+
5
+ __all__ = ["TrainingConfig", "train_dqn"]
traffic_rl/training/trainer.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+
7
+ from traffic_rl.agent.dqn_agent import DQNAgent
8
+ from traffic_rl.env.traffic_env import TrafficEnv
9
+
10
+
11
+ @dataclass
12
+ class TrainingConfig:
13
+ episodes: int = 120
14
+ max_steps: int = 120
15
+ gamma: float = 0.99
16
+ learning_rate: float = 1e-3
17
+ epsilon_start: float = 1.0
18
+ epsilon_end: float = 0.05
19
+ epsilon_decay: float = 0.98
20
+ batch_size: int = 64
21
+ buffer_size: int = 50000
22
+ target_sync_interval: int = 10
23
+ seed: int = 42
24
+
25
+
26
+ def _epsilon_for_episode(config: TrainingConfig, episode_idx: int) -> float:
27
+ decayed = config.epsilon_start * (config.epsilon_decay**episode_idx)
28
+ return float(max(config.epsilon_end, decayed))
29
+
30
+
31
+ def train_dqn(env: TrafficEnv, config: TrainingConfig | None = None) -> tuple[DQNAgent, dict[str, list[float]]]:
32
+ cfg = config or TrainingConfig()
33
+
34
+ agent = DQNAgent(
35
+ state_dim=10,
36
+ action_dim=3,
37
+ learning_rate=cfg.learning_rate,
38
+ gamma=cfg.gamma,
39
+ buffer_size=cfg.buffer_size,
40
+ batch_size=cfg.batch_size,
41
+ seed=cfg.seed,
42
+ )
43
+
44
+ history: dict[str, list[float]] = {
45
+ "episode_reward": [],
46
+ "avg_queue": [],
47
+ "avg_wait": [],
48
+ "throughput": [],
49
+ "loss": [],
50
+ "epsilon": [],
51
+ }
52
+
53
+ for episode in range(cfg.episodes):
54
+ state = env.reset()
55
+ epsilon = _epsilon_for_episode(cfg, episode)
56
+
57
+ total_reward = 0.0
58
+ step_queue = []
59
+ step_wait = []
60
+ total_throughput = 0
61
+
62
+ for _step in range(min(cfg.max_steps, env.config.max_steps)):
63
+ action = agent.select_action(state, epsilon=epsilon)
64
+ next_state, reward, done, info = env.step(action)
65
+
66
+ agent.store_transition(state, action, reward, next_state, done)
67
+ loss = agent.update()
68
+
69
+ if loss is not None:
70
+ history["loss"].append(loss)
71
+
72
+ total_reward += reward
73
+ step_queue.append(float(info["queue_sum"]))
74
+ step_wait.append(float(info["waiting_sum"]))
75
+ total_throughput += int(info["throughput"])
76
+
77
+ state = next_state
78
+ if done:
79
+ break
80
+
81
+ if (episode + 1) % cfg.target_sync_interval == 0:
82
+ agent.sync_target()
83
+
84
+ history["episode_reward"].append(float(total_reward))
85
+ history["avg_queue"].append(float(np.mean(step_queue) if step_queue else 0.0))
86
+ history["avg_wait"].append(float(np.mean(step_wait) if step_wait else 0.0))
87
+ history["throughput"].append(float(total_throughput))
88
+ history["epsilon"].append(epsilon)
89
+
90
+ return agent, history
traffic_rl/visualization/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Visualization helpers."""
2
+
3
+ from .dashboard import plot_comparison, plot_training_history, plot_trajectory
4
+
5
+ __all__ = ["plot_training_history", "plot_comparison", "plot_trajectory"]
traffic_rl/visualization/dashboard.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import matplotlib
6
+
7
+ matplotlib.use("Agg")
8
+ import matplotlib.pyplot as plt
9
+
10
+
11
+ def _ensure_dir(path: str | Path) -> Path:
12
+ p = Path(path)
13
+ p.mkdir(parents=True, exist_ok=True)
14
+ return p
15
+
16
+
17
+ def plot_training_history(history: dict[str, list[float]], output_dir: str | Path = "outputs") -> dict[str, str]:
18
+ out = _ensure_dir(output_dir)
19
+
20
+ fig, axes = plt.subplots(2, 2, figsize=(12, 8), constrained_layout=True)
21
+
22
+ axes[0, 0].plot(history.get("episode_reward", []), color="#1f77b4")
23
+ axes[0, 0].set_title("Episode Reward")
24
+ axes[0, 0].set_xlabel("Episode")
25
+
26
+ axes[0, 1].plot(history.get("avg_queue", []), color="#d62728")
27
+ axes[0, 1].set_title("Average Queue Length")
28
+ axes[0, 1].set_xlabel("Episode")
29
+
30
+ axes[1, 0].plot(history.get("throughput", []), color="#2ca02c")
31
+ axes[1, 0].set_title("Throughput")
32
+ axes[1, 0].set_xlabel("Episode")
33
+
34
+ axes[1, 1].plot(history.get("epsilon", []), color="#9467bd")
35
+ axes[1, 1].set_title("Exploration (Epsilon)")
36
+ axes[1, 1].set_xlabel("Episode")
37
+
38
+ path = out / "training_history.png"
39
+ fig.savefig(path, dpi=160)
40
+ plt.close(fig)
41
+
42
+ return {"training_history": str(path)}
43
+
44
+
45
+ def plot_comparison(
46
+ baseline: dict[str, float],
47
+ rl: dict[str, float],
48
+ output_dir: str | Path = "outputs",
49
+ ) -> dict[str, str]:
50
+ out = _ensure_dir(output_dir)
51
+
52
+ metrics = ["avg_waiting_time", "avg_queue_length", "throughput", "ambulance_clearances"]
53
+ labels = ["Avg Wait", "Avg Queue", "Throughput", "Ambulance Clears"]
54
+
55
+ x = range(len(metrics))
56
+ width = 0.35
57
+
58
+ fig, ax = plt.subplots(figsize=(10, 5), constrained_layout=True)
59
+ ax.bar([i - width / 2 for i in x], [baseline[m] for m in metrics], width, label="Fixed")
60
+ ax.bar([i + width / 2 for i in x], [rl[m] for m in metrics], width, label="RL")
61
+ ax.set_xticks(list(x))
62
+ ax.set_xticklabels(labels)
63
+ ax.set_title("RL vs Fixed-Time Controller")
64
+ ax.legend()
65
+
66
+ path = out / "policy_comparison.png"
67
+ fig.savefig(path, dpi=160)
68
+ plt.close(fig)
69
+
70
+ return {"policy_comparison": str(path)}
71
+
72
+
73
+ def plot_trajectory(trace: dict[str, list[float]], output_dir: str | Path = "outputs", name: str = "trajectory") -> dict[str, str]:
74
+ out = _ensure_dir(output_dir)
75
+
76
+ fig, axes = plt.subplots(3, 1, figsize=(10, 8), constrained_layout=True)
77
+
78
+ axes[0].plot(trace.get("queue_sum", []), color="#ff7f0e")
79
+ axes[0].set_title("Queue Sum")
80
+
81
+ axes[1].plot(trace.get("phase", []), color="#17becf")
82
+ axes[1].set_title("Signal Phase")
83
+
84
+ axes[2].plot(trace.get("reward", []), color="#1f77b4")
85
+ axes[2].set_title("Reward")
86
+ axes[2].set_xlabel("Step")
87
+
88
+ path = out / f"{name}.png"
89
+ fig.savefig(path, dpi=160)
90
+ plt.close(fig)
91
+
92
+ return {name: str(path)}