amanmurari commited on
Commit
b6f80c5
·
verified ·
1 Parent(s): 9b5744e

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ LABEL maintainer="OpenEnv Hackathon"
4
+ LABEL description="Autonomous Traffic Control – OpenEnv Environment (self-contained)"
5
+
6
+ WORKDIR /app
7
+
8
+ # ── Install system deps ────────────────────────────────────────────────────
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ build-essential \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # ── Copy packaging manifest first (layer-cache friendly) ──────────────────
14
+ COPY pyproject.toml README.md ./
15
+
16
+ # ── Install Python deps ────────────────────────────────────────────────────
17
+ RUN pip install --no-cache-dir \
18
+ "openenv-core[core]>=0.2.2" \
19
+ "openai>=1.0.0" \
20
+ "gradio>=4.0.0" \
21
+ "numpy>=1.24.0" \
22
+ "python-dotenv>=1.0.0" \
23
+ "fastapi>=0.104.0" \
24
+ "uvicorn[standard]>=0.24.0" \
25
+ "pydantic>=2.5.0" \
26
+ "requests>=2.31.0" \
27
+ "python-multipart>=0.0.6"
28
+
29
+ # ── Copy source (self-contained; no root-level deps needed) ───────────────
30
+ COPY models.py ./traffic_control/models.py
31
+ COPY environment.py ./traffic_control/environment.py
32
+ COPY tasks.py ./traffic_control/tasks.py
33
+ COPY client.py ./traffic_control/client.py
34
+ COPY __init__.py ./traffic_control/__init__.py
35
+ COPY inference.py ./traffic_control/inference.py
36
+ COPY openenv.yaml ./traffic_control/openenv.yaml
37
+ COPY server/ ./traffic_control/server/
38
+
39
+ # ── Create package anchor so Python treats traffic_control/ as the package ─
40
+ RUN echo "" > ./traffic_control/__init__.py || true
41
+
42
+ # ── Install the package in editable mode ──────────────────────────────────
43
+ RUN pip install --no-cache-dir -e .
44
+
45
+ # ── Runtime config ────────────────────────────────────────────────────────
46
+ ENV WORKERS=2
47
+ ENV PORT=8000
48
+ ENV PYTHONDONTWRITEBYTECODE=1
49
+ ENV PYTHONUNBUFFERED=1
50
+
51
+ EXPOSE 8000
52
+
53
+ HEALTHCHECK --interval=15s --timeout=5s --start-period=15s --retries=3 \
54
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
55
+
56
+ ENV ENABLE_WEB_INTERFACE=true
57
+ CMD ["sh", "-c", \
58
+ "uvicorn traffic_control.server.app:app \
59
+ --host 0.0.0.0 \
60
+ --port ${PORT} \
61
+ --workers ${WORKERS}"]
README.md CHANGED
@@ -1,10 +1,255 @@
1
  ---
2
- title: Sst Hack
3
- emoji: 💻
4
  colorFrom: gray
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Traffic Control Environment Server
3
+ emoji: 🎯
4
  colorFrom: gray
5
+ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Traffic Control Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the Traffic Control environment is through the `TrafficControlEnv` class:
21
+
22
+ ```python
23
+ from traffic_control import TrafficControlAction, TrafficControlEnv
24
+
25
+ try:
26
+ # Create environment from Docker image
27
+ traffic_controlenv = TrafficControlEnv.from_docker_image("traffic_control-env:latest")
28
+
29
+ # Reset
30
+ result = traffic_controlenv.reset()
31
+ print(f"Reset: {result.observation.echoed_message}")
32
+
33
+ # Send multiple messages
34
+ messages = ["Hello, World!", "Testing echo", "Final message"]
35
+
36
+ for msg in messages:
37
+ result = traffic_controlenv.step(TrafficControlAction(message=msg))
38
+ print(f"Sent: '{msg}'")
39
+ print(f" → Echoed: '{result.observation.echoed_message}'")
40
+ print(f" → Length: {result.observation.message_length}")
41
+ print(f" → Reward: {result.reward}")
42
+
43
+ finally:
44
+ # Always clean up
45
+ traffic_controlenv.close()
46
+ ```
47
+
48
+ That's it! The `TrafficControlEnv.from_docker_image()` method handles:
49
+ - Starting the Docker container
50
+ - Waiting for the server to be ready
51
+ - Connecting to the environment
52
+ - Container cleanup when you call `close()`
53
+
54
+ ## Building the Docker Image
55
+
56
+ Before using the environment, you need to build the Docker image:
57
+
58
+ ```bash
59
+ # From project root
60
+ docker build -t traffic_control-env:latest -f server/Dockerfile .
61
+ ```
62
+
63
+ ## Deploying to Hugging Face Spaces
64
+
65
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
+
67
+ ```bash
68
+ # From the environment directory (where openenv.yaml is located)
69
+ openenv push
70
+
71
+ # Or specify options
72
+ openenv push --namespace my-org --private
73
+ ```
74
+
75
+ The `openenv push` command will:
76
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
+ 3. Upload to Hugging Face (ensuring you're logged in)
79
+
80
+ ### Prerequisites
81
+
82
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
83
+
84
+ ### Options
85
+
86
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
+ - `--private`: Deploy the space as private (default: public)
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
+ openenv push
96
+
97
+ # Push to a specific repository
98
+ openenv push --repo-id my-org/my-env
99
+
100
+ # Push with a custom base image
101
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
+
103
+ # Push as a private space
104
+ openenv push --private
105
+
106
+ # Combine options
107
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
108
+ ```
109
+
110
+ After deployment, your space will be available at:
111
+ `https://huggingface.co/spaces/<repo-id>`
112
+
113
+ The deployed space includes:
114
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
+ - **Health Check** at `/health` - Container health monitoring
117
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
118
+
119
+ ## Environment Details
120
+
121
+ ### Action
122
+ **TrafficControlAction**: Contains a single field
123
+ - `message` (str) - The message to echo back
124
+
125
+ ### Observation
126
+ **TrafficControlObservation**: Contains the echo response and metadata
127
+ - `echoed_message` (str) - The message echoed back
128
+ - `message_length` (int) - Length of the message
129
+ - `reward` (float) - Reward based on message length (length × 0.1)
130
+ - `done` (bool) - Always False for echo environment
131
+ - `metadata` (dict) - Additional info like step count
132
+
133
+ ### Reward
134
+ The reward is calculated as: `message_length × 0.1`
135
+ - "Hi" → reward: 0.2
136
+ - "Hello, World!" → reward: 1.3
137
+ - Empty message → reward: 0.0
138
+
139
+ ## Advanced Usage
140
+
141
+ ### Connecting to an Existing Server
142
+
143
+ If you already have a Traffic Control environment server running, you can connect directly:
144
+
145
+ ```python
146
+ from traffic_control import TrafficControlEnv
147
+
148
+ # Connect to existing server
149
+ traffic_controlenv = TrafficControlEnv(base_url="<ENV_HTTP_URL_HERE>")
150
+
151
+ # Use as normal
152
+ result = traffic_controlenv.reset()
153
+ result = traffic_controlenv.step(TrafficControlAction(message="Hello!"))
154
+ ```
155
+
156
+ Note: When connecting to an existing server, `traffic_controlenv.close()` will NOT stop the server.
157
+
158
+ ### Using the Context Manager
159
+
160
+ The client supports context manager usage for automatic connection management:
161
+
162
+ ```python
163
+ from traffic_control import TrafficControlAction, TrafficControlEnv
164
+
165
+ # Connect with context manager (auto-connects and closes)
166
+ with TrafficControlEnv(base_url="http://localhost:8000") as env:
167
+ result = env.reset()
168
+ print(f"Reset: {result.observation.echoed_message}")
169
+ # Multiple steps with low latency
170
+ for msg in ["Hello", "World", "!"]:
171
+ result = env.step(TrafficControlAction(message=msg))
172
+ print(f"Echoed: {result.observation.echoed_message}")
173
+ ```
174
+
175
+ The client uses WebSocket connections for:
176
+ - **Lower latency**: No HTTP connection overhead per request
177
+ - **Persistent session**: Server maintains your environment state
178
+ - **Efficient for episodes**: Better for many sequential steps
179
+
180
+ ### Concurrent WebSocket Sessions
181
+
182
+ The server supports multiple concurrent WebSocket connections. To enable this,
183
+ modify `server/app.py` to use factory mode:
184
+
185
+ ```python
186
+ # In server/app.py - use factory mode for concurrent sessions
187
+ app = create_app(
188
+ TrafficControlEnvironment, # Pass class, not instance
189
+ TrafficControlAction,
190
+ TrafficControlObservation,
191
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
192
+ )
193
+ ```
194
+
195
+ Then multiple clients can connect simultaneously:
196
+
197
+ ```python
198
+ from traffic_control import TrafficControlAction, TrafficControlEnv
199
+ from concurrent.futures import ThreadPoolExecutor
200
+
201
+ def run_episode(client_id: int):
202
+ with TrafficControlEnv(base_url="http://localhost:8000") as env:
203
+ result = env.reset()
204
+ for i in range(10):
205
+ result = env.step(TrafficControlAction(message=f"Client {client_id}, step {i}"))
206
+ return client_id, result.observation.message_length
207
+
208
+ # Run 4 episodes concurrently
209
+ with ThreadPoolExecutor(max_workers=4) as executor:
210
+ results = list(executor.map(run_episode, range(4)))
211
+ ```
212
+
213
+ ## Development & Testing
214
+
215
+ ### Direct Environment Testing
216
+
217
+ Test the environment logic directly without starting the HTTP server:
218
+
219
+ ```bash
220
+ # From the server directory
221
+ python3 server/traffic_control_environment.py
222
+ ```
223
+
224
+ This verifies that:
225
+ - Environment resets correctly
226
+ - Step executes actions properly
227
+ - State tracking works
228
+ - Rewards are calculated correctly
229
+
230
+ ### Running Locally
231
+
232
+ Run the server locally for development:
233
+
234
+ ```bash
235
+ uvicorn server.app:app --reload
236
+ ```
237
+
238
+ ## Project Structure
239
+
240
+ ```
241
+ traffic_control/
242
+ ├── .dockerignore # Docker build exclusions
243
+ ├── __init__.py # Module exports
244
+ ├── README.md # This file
245
+ ├── openenv.yaml # OpenEnv manifest
246
+ ├── pyproject.toml # Project metadata and dependencies
247
+ ├── uv.lock # Locked dependencies (generated)
248
+ ├── client.py # TrafficControlEnv client
249
+ ├── models.py # Action and Observation models
250
+ └── server/
251
+ ├── __init__.py # Server module exports
252
+ ├── traffic_control_environment.py # Core environment logic
253
+ ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
254
+ └── Dockerfile # Container image definition
255
+ ```
__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Traffic Control Environment package."""
2
+
3
+ from .models import TrafficAction, TrafficObservation, TrafficState
4
+ from .client import TrafficControlEnv
5
+ from .environment import TrafficControlEnvironment
6
+
7
+ __all__ = [
8
+ "TrafficAction",
9
+ "TrafficObservation",
10
+ "TrafficState",
11
+ "TrafficControlEnv",
12
+ "TrafficControlEnvironment",
13
+ ]
client.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP/WebSocket client for the Autonomous Traffic Control environment."""
2
+
3
+ from typing import Any, Dict
4
+ from openenv.core.env_client import EnvClient, StepResult
5
+
6
+ from .models import TrafficAction, TrafficObservation, TrafficState
7
+
8
+
9
+ class TrafficControlEnv(EnvClient[TrafficAction, TrafficObservation, TrafficState]):
10
+ """
11
+ Client for the Autonomous Traffic Control environment.
12
+
13
+ Inherits all openenv-core EnvClient functionality:
14
+ - async context manager
15
+ - .sync() wrapper for synchronous use
16
+ - reset() / step() / state()
17
+ - from_docker_image() for local Docker deployment
18
+ - from_env() for HuggingFace Space deployment
19
+
20
+ Example (sync):
21
+ with TrafficControlEnv(base_url="http://localhost:8000").sync() as env:
22
+ obs = env.reset(task_id="basic_flow", seed=42)
23
+ while not obs.done:
24
+ obs = env.step(TrafficAction(light_phase=0))
25
+
26
+ Example (async):
27
+ async with TrafficControlEnv(base_url="http://localhost:8000") as env:
28
+ obs = await env.reset(task_id="emergency_priority", seed=0)
29
+ while not obs.done:
30
+ obs = await env.step(TrafficAction(light_phase=1))
31
+ """
32
+
33
+ def _step_payload(self, action: TrafficAction) -> Dict[str, Any]:
34
+ return action.model_dump()
35
+
36
+ def _parse_result(self, payload: Dict[str, Any]) -> StepResult[TrafficObservation]:
37
+ obs = TrafficObservation(**payload["observation"])
38
+ return StepResult(
39
+ observation=obs,
40
+ reward=payload.get("reward"),
41
+ done=payload.get("done", False),
42
+ )
43
+
44
+ def _parse_state(self, payload: Dict[str, Any]) -> TrafficState:
45
+ return TrafficState(**payload)
environment.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core simulation for the Autonomous Traffic Control Environment.
3
+
4
+ Implements openenv-core's Environment interface so it works directly
5
+ with create_app() — no adapters needed.
6
+
7
+ Simulates a 4-way intersection with:
8
+ - Poisson vehicle arrivals per approach
9
+ - Emergency vehicles with urgency levels (0-10)
10
+ - Yellow-light transition state machine (2-step yellow)
11
+ - Traffic-surge events (hard task only)
12
+ - Multi-objective reward function
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ import random
19
+ import uuid
20
+ from dataclasses import dataclass
21
+ from enum import IntEnum
22
+ from typing import Dict, List, Optional, Set, Tuple
23
+
24
+ from openenv.core.env_server.interfaces import Environment
25
+
26
+ from .models import (
27
+ TrafficAction,
28
+ TrafficObservation,
29
+ TrafficState,
30
+ PHASE_NS_GREEN,
31
+ PHASE_EW_GREEN,
32
+ PHASE_ALL_RED,
33
+ PHASE_NS_YELLOW,
34
+ PHASE_EW_YELLOW,
35
+ )
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Internal enums
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class VehicleType(IntEnum):
43
+ CAR = 0
44
+ BUS = 1
45
+ EMERGENCY = 2
46
+
47
+
48
+ class Direction(IntEnum):
49
+ NORTH = 0
50
+ SOUTH = 1
51
+ EAST = 2
52
+ WEST = 3
53
+
54
+
55
+ class LightPhase(IntEnum):
56
+ NS_GREEN = PHASE_NS_GREEN
57
+ EW_GREEN = PHASE_EW_GREEN
58
+ ALL_RED = PHASE_ALL_RED
59
+ NS_YELLOW = PHASE_NS_YELLOW
60
+ EW_YELLOW = PHASE_EW_YELLOW
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Phase transition tables
65
+ # ---------------------------------------------------------------------------
66
+
67
+ PHASE_ALLOWS: Dict[LightPhase, Set[int]] = {
68
+ LightPhase.NS_GREEN: {Direction.NORTH, Direction.SOUTH},
69
+ LightPhase.EW_GREEN: {Direction.EAST, Direction.WEST},
70
+ LightPhase.ALL_RED: set(),
71
+ LightPhase.NS_YELLOW: {Direction.NORTH, Direction.SOUTH},
72
+ LightPhase.EW_YELLOW: {Direction.EAST, Direction.WEST},
73
+ }
74
+
75
+ PHASE_FLOW_RATE: Dict[LightPhase, int] = {
76
+ LightPhase.NS_GREEN: 3,
77
+ LightPhase.EW_GREEN: 3,
78
+ LightPhase.ALL_RED: 0,
79
+ LightPhase.NS_YELLOW: 1,
80
+ LightPhase.EW_YELLOW: 1,
81
+ }
82
+
83
+ YELLOW_DURATION = 2
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Task configurations
88
+ # ---------------------------------------------------------------------------
89
+
90
+ TASK_CONFIGS: Dict[str, dict] = {
91
+ "basic_flow": {
92
+ "vehicle_arrival_rate": 0.4,
93
+ "emergency_arrival_rate": 0.0,
94
+ "emergency_urgency_range": (0, 0),
95
+ "max_steps": 200,
96
+ "max_queue_per_lane": 20,
97
+ "surge_probability": 0.0,
98
+ "surge_multiplier": 1.0,
99
+ },
100
+ "emergency_priority": {
101
+ "vehicle_arrival_rate": 0.5,
102
+ "emergency_arrival_rate": 0.015,
103
+ "emergency_urgency_range": (7, 10),
104
+ "max_steps": 300,
105
+ "max_queue_per_lane": 20,
106
+ "surge_probability": 0.0,
107
+ "surge_multiplier": 1.0,
108
+ },
109
+ "dynamic_scenarios": {
110
+ "vehicle_arrival_rate": 0.7,
111
+ "emergency_arrival_rate": 0.035,
112
+ "emergency_urgency_range": (8, 10),
113
+ "max_steps": 400,
114
+ "max_queue_per_lane": 30,
115
+ "surge_probability": 0.04,
116
+ "surge_multiplier": 3.0,
117
+ },
118
+ }
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Internal vehicle dataclass
123
+ # ---------------------------------------------------------------------------
124
+
125
+ @dataclass
126
+ class Vehicle:
127
+ vehicle_type: VehicleType
128
+ direction: Direction
129
+ waiting_time: int = 0
130
+ urgency: int = 0
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Environment
135
+ # ---------------------------------------------------------------------------
136
+
137
+ class TrafficControlEnvironment(Environment):
138
+ """
139
+ OpenEnv-compliant Autonomous Traffic Control environment.
140
+
141
+ Inherits from openenv.core.env_server.interfaces.Environment,
142
+ making it compatible with openenv-core's create_app() factory.
143
+
144
+ Methods
145
+ -------
146
+ reset(seed, episode_id, **kwargs) -> TrafficObservation
147
+ step(action) -> TrafficObservation
148
+ state (property) -> TrafficState
149
+ """
150
+
151
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
152
+
153
+ def __init__(self, task_id: str = "basic_flow") -> None:
154
+ if task_id not in TASK_CONFIGS:
155
+ raise ValueError(
156
+ f"Unknown task_id '{task_id}'. "
157
+ f"Valid options: {list(TASK_CONFIGS.keys())}"
158
+ )
159
+ self.task_id = task_id
160
+ self._cfg = TASK_CONFIGS[task_id]
161
+ self._rng = random.Random()
162
+
163
+ self._episode_id: str = ""
164
+ self._step_count: int = 0
165
+ self._queues: List[List[Vehicle]] = [[] for _ in range(4)]
166
+ self._current_phase: LightPhase = LightPhase.NS_GREEN
167
+ self._time_in_phase: int = 0
168
+ self._pending_phase: Optional[int] = None
169
+
170
+ self._total_vehicles_passed: int = 0
171
+ self._total_emergency_passed: int = 0
172
+ self._total_waiting_time: float = 0.0
173
+ self._total_emergency_delay: float = 0.0
174
+ self._total_collisions: int = 0
175
+ self._total_phase_changes: int = 0
176
+
177
+ # ------------------------------------------------------------------
178
+ # openenv-core Environment interface
179
+ # ------------------------------------------------------------------
180
+
181
+ def reset(
182
+ self,
183
+ seed: Optional[int] = None,
184
+ episode_id: Optional[str] = None,
185
+ task_id: Optional[str] = None,
186
+ **kwargs,
187
+ ) -> TrafficObservation:
188
+ """Start a fresh episode."""
189
+ if task_id and task_id in TASK_CONFIGS:
190
+ self.task_id = task_id
191
+ self._cfg = TASK_CONFIGS[task_id]
192
+
193
+ self._rng = random.Random(seed)
194
+ self._episode_id = episode_id or str(uuid.uuid4())
195
+ self._step_count = 0
196
+ self._queues = [[] for _ in range(4)]
197
+ self._current_phase = LightPhase.NS_GREEN
198
+ self._time_in_phase = 0
199
+ self._pending_phase = None
200
+
201
+ self._total_vehicles_passed = 0
202
+ self._total_emergency_passed = 0
203
+ self._total_waiting_time = 0.0
204
+ self._total_emergency_delay = 0.0
205
+ self._total_collisions = 0
206
+ self._total_phase_changes = 0
207
+
208
+ return self._build_obs(0, 0, 0.0, False, 0.0, False)
209
+
210
+ def step(self, action: TrafficAction) -> TrafficObservation: # type: ignore[override]
211
+ """Execute one simulation step."""
212
+ self._step_count += 1
213
+
214
+ self._spawn_vehicles()
215
+ phase_changed = self._apply_action(action)
216
+ self._advance_phase()
217
+ vehicles_passed, emergency_passed = self._flow_traffic()
218
+ waiting_delta = self._tick_waiting_times()
219
+ collision = self._check_collision()
220
+ reward = self._compute_reward(
221
+ vehicles_passed, emergency_passed, waiting_delta, collision, phase_changed
222
+ )
223
+
224
+ self._total_vehicles_passed += vehicles_passed
225
+ self._total_emergency_passed += emergency_passed
226
+ self._total_waiting_time += waiting_delta
227
+ if collision:
228
+ self._total_collisions += 1
229
+
230
+ done = collision or self._step_count >= self._cfg["max_steps"]
231
+ return self._build_obs(vehicles_passed, emergency_passed, waiting_delta, collision, reward, done)
232
+
233
+ @property
234
+ def state(self) -> TrafficState:
235
+ """Return cumulative episode-level state."""
236
+ return TrafficState(
237
+ episode_id=self._episode_id,
238
+ step_count=self._step_count,
239
+ task_id=self.task_id,
240
+ total_vehicles_passed=self._total_vehicles_passed,
241
+ total_emergency_passed=self._total_emergency_passed,
242
+ total_waiting_time=self._total_waiting_time,
243
+ total_emergency_delay=self._total_emergency_delay,
244
+ total_collisions=self._total_collisions,
245
+ total_phase_changes=self._total_phase_changes,
246
+ )
247
+
248
+ # ------------------------------------------------------------------
249
+ # Simulation internals
250
+ # ------------------------------------------------------------------
251
+
252
+ def _spawn_vehicles(self) -> None:
253
+ arr = self._cfg["vehicle_arrival_rate"]
254
+ em = self._cfg["emergency_arrival_rate"]
255
+ urg = self._cfg["emergency_urgency_range"]
256
+ surge_p = self._cfg["surge_probability"]
257
+ surge_m = self._cfg["surge_multiplier"]
258
+ max_q = self._cfg["max_queue_per_lane"]
259
+
260
+ surge_dir = -1
261
+ surge_extra = 0
262
+ if surge_p > 0.0 and self._rng.random() < surge_p:
263
+ surge_dir = self._rng.randint(0, 3)
264
+ surge_extra = max(0, int(self._rng.gauss(3, 1) * surge_m))
265
+
266
+ for d in range(4):
267
+ n = self._poisson(arr)
268
+ if d == surge_dir:
269
+ n += surge_extra
270
+ for _ in range(n):
271
+ if len(self._queues[d]) < max_q:
272
+ vt = VehicleType.BUS if self._rng.random() < 0.10 else VehicleType.CAR
273
+ self._queues[d].append(Vehicle(vt, Direction(d)))
274
+
275
+ if em > 0.0 and self._rng.random() < em:
276
+ if len(self._queues[d]) < max_q:
277
+ urgency = self._rng.randint(urg[0], urg[1])
278
+ self._queues[d].insert(
279
+ 0,
280
+ Vehicle(VehicleType.EMERGENCY, Direction(d), urgency=urgency),
281
+ )
282
+
283
+ def _apply_action(self, action: TrafficAction) -> bool:
284
+ req = action.light_phase
285
+ if req not in (PHASE_NS_GREEN, PHASE_EW_GREEN, PHASE_ALL_RED):
286
+ return False
287
+ if self._current_phase in (LightPhase.NS_YELLOW, LightPhase.EW_YELLOW):
288
+ return False
289
+ current_base = int(self._current_phase)
290
+ if current_base == req:
291
+ return False
292
+
293
+ self._total_phase_changes += 1
294
+ self._pending_phase = req
295
+
296
+ if req == PHASE_ALL_RED:
297
+ self._current_phase = LightPhase.ALL_RED
298
+ self._time_in_phase = 0
299
+ self._pending_phase = None
300
+ elif self._current_phase == LightPhase.NS_GREEN:
301
+ self._current_phase = LightPhase.NS_YELLOW
302
+ self._time_in_phase = 0
303
+ elif self._current_phase == LightPhase.EW_GREEN:
304
+ self._current_phase = LightPhase.EW_YELLOW
305
+ self._time_in_phase = 0
306
+ elif self._current_phase == LightPhase.ALL_RED:
307
+ self._current_phase = LightPhase(req)
308
+ self._time_in_phase = 0
309
+ self._pending_phase = None
310
+
311
+ return True
312
+
313
+ def _advance_phase(self) -> None:
314
+ self._time_in_phase += 1
315
+ if self._current_phase in (LightPhase.NS_YELLOW, LightPhase.EW_YELLOW):
316
+ if self._time_in_phase >= YELLOW_DURATION:
317
+ target = self._pending_phase if self._pending_phase is not None else PHASE_ALL_RED
318
+ self._current_phase = LightPhase(target)
319
+ self._time_in_phase = 0
320
+ self._pending_phase = None
321
+
322
+ def _flow_traffic(self) -> Tuple[int, int]:
323
+ allowed = PHASE_ALLOWS[self._current_phase]
324
+ flow_rate = PHASE_FLOW_RATE[self._current_phase]
325
+ vehicles_passed = 0
326
+ emergency_passed = 0
327
+
328
+ for d in allowed:
329
+ queue = self._queues[int(d)]
330
+ passed_dir = 0
331
+ while queue and passed_dir < flow_rate:
332
+ vehicle = queue.pop(0)
333
+ passed_dir += 1
334
+ if vehicle.vehicle_type == VehicleType.EMERGENCY:
335
+ emergency_passed += 1
336
+ else:
337
+ vehicles_passed += 1
338
+
339
+ return vehicles_passed, emergency_passed
340
+
341
+ def _tick_waiting_times(self) -> float:
342
+ total = 0.0
343
+ for d in range(4):
344
+ for v in self._queues[d]:
345
+ v.waiting_time += 1
346
+ total += 1.0
347
+ if v.vehicle_type == VehicleType.EMERGENCY:
348
+ self._total_emergency_delay += 1.0
349
+ return total
350
+
351
+ def _check_collision(self) -> bool:
352
+ total_queued = sum(len(q) for q in self._queues)
353
+ if total_queued > 40 and self._time_in_phase > 20:
354
+ return self._rng.random() < 0.04
355
+ return False
356
+
357
+ def _compute_reward(
358
+ self,
359
+ vehicles_passed: int,
360
+ emergency_passed: int,
361
+ waiting_delta: float,
362
+ collision: bool,
363
+ phase_changed: bool,
364
+ ) -> float:
365
+ r = vehicles_passed * 0.20
366
+ r += emergency_passed * 10.0
367
+ r -= waiting_delta * 0.05
368
+
369
+ for d in range(4):
370
+ for v in self._queues[d]:
371
+ if v.vehicle_type == VehicleType.EMERGENCY:
372
+ r -= v.urgency * 0.4
373
+
374
+ if collision:
375
+ r -= 200.0
376
+
377
+ if phase_changed:
378
+ p = int(self._current_phase)
379
+ if p == PHASE_NS_GREEN:
380
+ if (len(self._queues[0]) + len(self._queues[1])) == 0:
381
+ r -= 0.5
382
+ elif p == PHASE_EW_GREEN:
383
+ if (len(self._queues[2]) + len(self._queues[3])) == 0:
384
+ r -= 0.5
385
+
386
+ return r
387
+
388
+ def _build_obs(
389
+ self,
390
+ vehicles_passed: int,
391
+ emergency_passed: int,
392
+ waiting_delta: float,
393
+ collision: bool,
394
+ reward: float,
395
+ done: bool,
396
+ ) -> TrafficObservation:
397
+ queue_lengths = []
398
+ emergency_queue = []
399
+ emergency_urgency = []
400
+
401
+ for d in range(4):
402
+ reg = sum(1 for v in self._queues[d] if v.vehicle_type != VehicleType.EMERGENCY)
403
+ em = sum(1 for v in self._queues[d] if v.vehicle_type == VehicleType.EMERGENCY)
404
+ max_u = max(
405
+ (v.urgency for v in self._queues[d] if v.vehicle_type == VehicleType.EMERGENCY),
406
+ default=0,
407
+ )
408
+ queue_lengths.append(reg)
409
+ emergency_queue.append(em)
410
+ emergency_urgency.append(max_u)
411
+
412
+ return TrafficObservation(
413
+ current_phase=int(self._current_phase),
414
+ time_in_phase=self._time_in_phase,
415
+ queue_lengths=queue_lengths,
416
+ emergency_queue=emergency_queue,
417
+ emergency_urgency=emergency_urgency,
418
+ vehicles_passed=vehicles_passed,
419
+ emergency_passed=emergency_passed,
420
+ total_waiting_time=waiting_delta,
421
+ collision=collision,
422
+ reward=reward,
423
+ done=done,
424
+ metadata={
425
+ "step_count": self._step_count,
426
+ "task_id": self.task_id,
427
+ },
428
+ )
429
+
430
+ def _poisson(self, lam: float) -> int:
431
+ if lam <= 0.0:
432
+ return 0
433
+ threshold = math.exp(-lam)
434
+ k, p = 0, 1.0
435
+ while p > threshold:
436
+ k += 1
437
+ p *= self._rng.random()
438
+ return k - 1
inference.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference Script — Autonomous Traffic Control OpenEnv Environment
3
+ =================================================================
4
+ Mandatory env variables:
5
+ API_BASE_URL LLM endpoint (default: https://api.openai.com/v1)
6
+ MODEL_NAME Model to use (default: gpt-4.1-mini)
7
+ HF_TOKEN Your Hugging Face / LLM API key ← REQUIRED
8
+
9
+ Optional:
10
+ SERVER_URL Running env server (default: http://localhost:8000)
11
+
12
+ Run:
13
+ HF_TOKEN=<key> python inference.py
14
+ HF_TOKEN=<key> SERVER_URL=http://localhost:8000 python inference.py
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import json
20
+ import textwrap
21
+ import requests as _http
22
+
23
+ # Allow running directly from traffic_control/ OR from its parent
24
+ _HERE = os.path.dirname(os.path.abspath(__file__))
25
+ _PARENT = os.path.dirname(_HERE)
26
+ for _p in (_HERE, _PARENT):
27
+ if _p not in sys.path:
28
+ sys.path.insert(0, _p)
29
+
30
+ try:
31
+ from dotenv import load_dotenv
32
+ load_dotenv()
33
+ except ImportError:
34
+ pass
35
+
36
+ from openai import OpenAI
37
+
38
+ # Import from within the self-contained package
39
+ try:
40
+ from traffic_control.client import TrafficControlEnv
41
+ from traffic_control.models import TrafficAction, TrafficObservation
42
+ except ImportError:
43
+ from client import TrafficControlEnv
44
+ from models import TrafficAction, TrafficObservation
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Configuration
48
+ # ---------------------------------------------------------------------------
49
+
50
+ API_BASE_URL: str = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
51
+ MODEL_NAME: str = os.getenv("MODEL_NAME", "gpt-4.1-mini")
52
+ HF_TOKEN: str = os.getenv("HF_TOKEN", "")
53
+ SERVER_URL: str = os.getenv("SERVER_URL", "http://localhost:8000")
54
+
55
+ if not HF_TOKEN:
56
+ raise ValueError("HF_TOKEN environment variable is required")
57
+
58
+ SEED = 42
59
+ MAX_TOKENS = 32
60
+ TEMPERATURE = 0.0
61
+
62
+ llm_client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Prompts
66
+ # ---------------------------------------------------------------------------
67
+
68
+ SYSTEM_PROMPT = textwrap.dedent("""
69
+ You are an Autonomous Traffic Control AI managing a 4-way intersection.
70
+
71
+ OBJECTIVE: Maximise vehicle throughput and prioritise emergency vehicles.
72
+
73
+ PHASES:
74
+ 0 = North-South Green (N/S vehicles may pass)
75
+ 1 = East-West Green (E/W vehicles may pass)
76
+ 2 = All Red (no vehicles pass — use only for emergency clearance)
77
+
78
+ STRATEGY:
79
+ 1. If any emergency vehicles are waiting, switch to their direction immediately.
80
+ 2. Otherwise, switch to the direction with the most queued vehicles.
81
+ 3. Avoid changing phase too frequently (wait ≥ 4 steps per phase).
82
+
83
+ OUTPUT: Reply with exactly one JSON object — no markdown, no explanation:
84
+ {"light_phase": <0, 1, or 2>}
85
+ """).strip()
86
+
87
+
88
+ def _build_prompt(obs: TrafficObservation) -> str:
89
+ q = obs.queue_lengths
90
+ em_q = obs.emergency_queue
91
+ em_u = obs.emergency_urgency
92
+ return textwrap.dedent(f"""
93
+ CURRENT STATE:
94
+ Active phase : {obs.current_phase}
95
+ Steps in phase : {obs.time_in_phase}
96
+ Regular queue : N={q[0]}, S={q[1]}, E={q[2]}, W={q[3]}
97
+ Emergency queue : N={em_q[0]}, S={em_q[1]}, E={em_q[2]}, W={em_q[3]}
98
+ Emergency urgency : N={em_u[0]}, S={em_u[1]}, E={em_u[2]}, W={em_u[3]}
99
+
100
+ Output exactly: {{"light_phase": 0}}
101
+ """).strip()
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Rule-based fallback (used when LLM call fails)
105
+ # ---------------------------------------------------------------------------
106
+
107
+ def _rule_based_action(obs: TrafficObservation) -> TrafficAction:
108
+ """Simple heuristic: emergency first, else highest queue."""
109
+ em_q = obs.emergency_queue
110
+ q = obs.queue_lengths
111
+
112
+ # Emergency vehicle present?
113
+ if sum(em_q) > 0:
114
+ if em_q[0] + em_q[1] >= em_q[2] + em_q[3]:
115
+ return TrafficAction(light_phase=0) # NS Green
116
+ else:
117
+ return TrafficAction(light_phase=1) # EW Green
118
+
119
+ # Highest queue direction
120
+ ns_total = q[0] + q[1]
121
+ ew_total = q[2] + q[3]
122
+ if obs.current_phase == 0 and obs.time_in_phase < 4:
123
+ return TrafficAction(light_phase=0)
124
+ if obs.current_phase == 1 and obs.time_in_phase < 4:
125
+ return TrafficAction(light_phase=1)
126
+ return TrafficAction(light_phase=0 if ns_total >= ew_total else 1)
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # LLM action
130
+ # ---------------------------------------------------------------------------
131
+
132
+ def get_llm_action(obs: TrafficObservation) -> TrafficAction:
133
+ try:
134
+ resp = llm_client.chat.completions.create(
135
+ model=MODEL_NAME,
136
+ messages=[
137
+ {"role": "system", "content": SYSTEM_PROMPT},
138
+ {"role": "user", "content": _build_prompt(obs)},
139
+ ],
140
+ response_format={"type": "json_object"},
141
+ temperature=TEMPERATURE,
142
+ max_tokens=MAX_TOKENS,
143
+ )
144
+ data = json.loads(resp.choices[0].message.content or "{}")
145
+ phase = int(data.get("light_phase", obs.current_phase))
146
+ phase = max(0, min(2, phase))
147
+ return TrafficAction(light_phase=phase)
148
+ except Exception:
149
+ return _rule_based_action(obs)
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Grade fetcher (calls /grade after episode ends)
153
+ # ---------------------------------------------------------------------------
154
+
155
+ def _fetch_score(task_id: str, state_payload: dict) -> float:
156
+ """Call the /grade endpoint and return a clamped (0.001, 0.999) score."""
157
+ try:
158
+ r = _http.post(
159
+ f"{SERVER_URL}/grade",
160
+ json={
161
+ "task_id": task_id,
162
+ "total_vehicles_passed": state_payload.get("total_vehicles_passed", 0),
163
+ "total_emergency_passed":state_payload.get("total_emergency_passed", 0),
164
+ "total_waiting_time": state_payload.get("total_waiting_time", 0.0),
165
+ "total_collisions": state_payload.get("total_collisions", 0),
166
+ "total_emergency_delay": state_payload.get("total_emergency_delay", 0.0),
167
+ "total_phase_changes": state_payload.get("total_phase_changes", 0),
168
+ "step_count": max(state_payload.get("step_count", 1), 1),
169
+ },
170
+ timeout=10,
171
+ )
172
+ if r.status_code == 200:
173
+ raw = float(r.json().get("score", 0.5))
174
+ return max(0.001, min(0.999, raw))
175
+ except Exception:
176
+ pass
177
+ return 0.5 # safe fallback
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Task runner
182
+ # ---------------------------------------------------------------------------
183
+
184
+ def run_task(task_id: str) -> None:
185
+ print(f"[START] task={task_id} env=traffic-control model={MODEL_NAME}")
186
+
187
+ rewards: list[float] = []
188
+ success = False
189
+
190
+ try:
191
+ with TrafficControlEnv(base_url=SERVER_URL).sync() as env:
192
+ step_result = env.reset(task_id=task_id, seed=SEED)
193
+ step = 1
194
+
195
+ while not step_result.done:
196
+ obs = step_result.observation
197
+ error_msg = "null"
198
+
199
+ try:
200
+ action = get_llm_action(obs)
201
+ except Exception as exc:
202
+ error_msg = str(exc).replace('"', "'").replace("\\", "")
203
+ action = _rule_based_action(obs)
204
+
205
+ action_str = f"TrafficAction(light_phase={action.light_phase})"
206
+
207
+ try:
208
+ step_result = env.step(action)
209
+ done_str = "true" if step_result.done else "false"
210
+ reward_val = step_result.reward if step_result.reward is not None else 0.0
211
+ rewards.append(reward_val)
212
+ print(
213
+ f"[STEP] step={step} action={action_str} "
214
+ f"reward={reward_val:.2f} done={done_str} error={error_msg}"
215
+ )
216
+ except Exception as exc:
217
+ env_error = str(exc).replace('"', "'").replace("\\", "")
218
+ print(
219
+ f"[STEP] step={step} action={action_str} "
220
+ f"reward=0.00 done=true error={env_error}"
221
+ )
222
+ break
223
+
224
+ step += 1
225
+
226
+ success = True
227
+
228
+ except Exception as exc:
229
+ print(f"[STEP] step=0 action=none reward=0.00 done=true error={exc}")
230
+ success = False
231
+
232
+ success_str = "true" if success else "false"
233
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards) if rewards else "0.00"
234
+
235
+ # Fetch final grade score from /grade endpoint
236
+ score = 0.5
237
+ try:
238
+ state_resp = _http.get(f"{SERVER_URL}/state", timeout=10)
239
+ if state_resp.status_code == 200:
240
+ score = _fetch_score(task_id, state_resp.json())
241
+ except Exception:
242
+ pass
243
+
244
+ print(f"[END] success={success_str} steps={len(rewards)} score={score:.3f} rewards={rewards_str}")
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Entry point
249
+ # ---------------------------------------------------------------------------
250
+
251
+ def main() -> None:
252
+ tasks = ["basic_flow", "emergency_priority", "dynamic_scenarios"]
253
+ for task in tasks:
254
+ run_task(task)
255
+ print() # blank line between tasks
256
+
257
+
258
+ if __name__ == "__main__":
259
+ main()
models.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Typed data models for the Autonomous Traffic Control Environment.
3
+
4
+ All Pydantic models extend openenv-core base types so the environment
5
+ is fully compliant with the OpenEnv specification.
6
+ """
7
+
8
+ from typing import List, Optional, Any, Dict
9
+ from openenv.core.env_server.types import Action, Observation, State
10
+ from pydantic import Field
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Phase / direction constants
15
+ # ---------------------------------------------------------------------------
16
+
17
+ PHASE_NS_GREEN = 0 # North-South green, East-West red
18
+ PHASE_EW_GREEN = 1 # East-West green, North-South red
19
+ PHASE_ALL_RED = 2 # All approaches red (emergency clearance)
20
+ PHASE_NS_YELLOW = 3 # North-South transitioning to red
21
+ PHASE_EW_YELLOW = 4 # East-West transitioning to red
22
+
23
+ DIRECTION_NORTH = 0
24
+ DIRECTION_SOUTH = 1
25
+ DIRECTION_EAST = 2
26
+ DIRECTION_WEST = 3
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Action
31
+ # ---------------------------------------------------------------------------
32
+
33
+ class TrafficAction(Action):
34
+ """
35
+ Agent action: set the desired traffic light phase.
36
+
37
+ light_phase:
38
+ 0 = NS_GREEN – North + South get green, East + West get red.
39
+ 1 = EW_GREEN – East + West get green, North + South get red.
40
+ 2 = ALL_RED – All approaches red; use for emergency clearance.
41
+ """
42
+ light_phase: int = Field(
43
+ ...,
44
+ ge=0, le=2,
45
+ description="Desired light phase: 0=NS_GREEN | 1=EW_GREEN | 2=ALL_RED",
46
+ )
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Observation
51
+ # ---------------------------------------------------------------------------
52
+
53
+ class TrafficObservation(Observation):
54
+ """
55
+ Full observation returned by reset() and step().
56
+
57
+ Directions index: 0=North, 1=South, 2=East, 3=West
58
+ The `done`, `reward`, and `metadata` fields are inherited from
59
+ openenv.core.env_server.types.Observation.
60
+ """
61
+
62
+ # -- Traffic-light state --
63
+ current_phase: int = Field(
64
+ default=0,
65
+ description="Active light phase: 0=NS_GREEN|1=EW_GREEN|2=ALL_RED|3=NS_YELLOW|4=EW_YELLOW",
66
+ )
67
+ time_in_phase: int = Field(
68
+ default=0,
69
+ description="Steps elapsed since last phase change",
70
+ )
71
+
72
+ # -- Vehicle queues (one per direction [N, S, E, W]) --
73
+ queue_lengths: List[int] = Field(
74
+ default_factory=lambda: [0, 0, 0, 0],
75
+ description="Regular vehicle count per approach [N, S, E, W]",
76
+ )
77
+ emergency_queue: List[int] = Field(
78
+ default_factory=lambda: [0, 0, 0, 0],
79
+ description="Emergency vehicle count per approach [N, S, E, W]",
80
+ )
81
+ emergency_urgency: List[int] = Field(
82
+ default_factory=lambda: [0, 0, 0, 0],
83
+ description="Max urgency (0-10) of waiting emergency vehicles per approach",
84
+ )
85
+
86
+ # -- Flow metrics for this step --
87
+ vehicles_passed: int = Field(default=0, description="Regular vehicles cleared this step")
88
+ emergency_passed: int = Field(default=0, description="Emergency vehicles cleared this step")
89
+
90
+ # -- Penalty signals --
91
+ total_waiting_time: float = Field(
92
+ default=0.0,
93
+ description="Sum of per-vehicle waiting increments this step",
94
+ )
95
+ collision: bool = Field(default=False, description="Gridlock-induced collision flag")
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # State
100
+ # ---------------------------------------------------------------------------
101
+
102
+ class TrafficState(State):
103
+ """
104
+ Episode-level cumulative state, returned by state().
105
+
106
+ The `episode_id` and `step_count` fields are inherited from
107
+ openenv.core.env_server.types.State.
108
+ """
109
+ task_id: str = Field(default="basic_flow", description="Active task ID")
110
+
111
+ # Cumulative episode metrics
112
+ total_vehicles_passed: int = Field(default=0)
113
+ total_emergency_passed: int = Field(default=0)
114
+ total_waiting_time: float = Field(default=0.0)
115
+ total_emergency_delay: float = Field(
116
+ default=0.0,
117
+ description="Steps emergency vehicles spent waiting",
118
+ )
119
+ total_collisions: int = Field(default=0)
120
+ total_phase_changes: int = Field(default=0)
openenv.yaml ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: traffic-control
2
+ name: Autonomous Traffic Control Environment
3
+ version: "1.0.0"
4
+ description: >
5
+ A 4-way intersection RL environment where an AI agent controls traffic lights
6
+ to maximise vehicle throughput and prioritise emergency vehicles.
7
+ Compliant with the OpenEnv reset / step / state API specification.
8
+
9
+ author: OpenEnv Hackathon Submission
10
+ tags:
11
+ - reinforcement-learning
12
+ - traffic-control
13
+ - emergency-vehicles
14
+ - autonomous-systems
15
+ - openenv
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Observation space
19
+ # ---------------------------------------------------------------------------
20
+ observation_space:
21
+ type: object
22
+ properties:
23
+ current_phase:
24
+ type: integer
25
+ enum: [0, 1, 2, 3, 4]
26
+ description: >
27
+ Active traffic-light phase.
28
+ 0=NS_GREEN | 1=EW_GREEN | 2=ALL_RED | 3=NS_YELLOW | 4=EW_YELLOW
29
+ time_in_phase:
30
+ type: integer
31
+ minimum: 0
32
+ description: Steps elapsed since the last phase change.
33
+ queue_lengths:
34
+ type: array
35
+ items: { type: integer, minimum: 0 }
36
+ minItems: 4
37
+ maxItems: 4
38
+ description: "Regular vehicle queue depth per approach [N, S, E, W]."
39
+ emergency_queue:
40
+ type: array
41
+ items: { type: integer, minimum: 0 }
42
+ minItems: 4
43
+ maxItems: 4
44
+ description: "Emergency vehicle count per approach [N, S, E, W]."
45
+ emergency_urgency:
46
+ type: array
47
+ items: { type: integer, minimum: 0, maximum: 10 }
48
+ minItems: 4
49
+ maxItems: 4
50
+ description: "Max urgency of waiting emergency vehicles per approach (0 = none)."
51
+ vehicles_passed:
52
+ type: integer
53
+ minimum: 0
54
+ description: Regular vehicles that cleared the intersection this step.
55
+ emergency_passed:
56
+ type: integer
57
+ minimum: 0
58
+ description: Emergency vehicles that cleared the intersection this step.
59
+ total_waiting_time:
60
+ type: number
61
+ description: Sum of per-vehicle waiting increments accumulated this step.
62
+ collision:
63
+ type: boolean
64
+ description: True if a gridlock-induced collision occurred this step.
65
+ reward:
66
+ type: number
67
+ description: Step reward computed by the environment.
68
+ done:
69
+ type: boolean
70
+ description: True when the episode has ended.
71
+ metadata:
72
+ type: object
73
+ description: Auxiliary info (step_count, task_id, …).
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Action space
77
+ # ---------------------------------------------------------------------------
78
+ action_space:
79
+ type: object
80
+ properties:
81
+ light_phase:
82
+ type: integer
83
+ enum: [0, 1, 2]
84
+ description: >
85
+ Desired traffic-light phase.
86
+ 0=NS_GREEN | 1=EW_GREEN | 2=ALL_RED
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Tasks
90
+ # ---------------------------------------------------------------------------
91
+ tasks:
92
+ - id: basic_flow
93
+ name: Basic Traffic Flow Management
94
+ difficulty: easy
95
+ description: >
96
+ Optimise vehicle throughput at a 4-way intersection with moderate,
97
+ consistent traffic and no emergency vehicles.
98
+ max_steps: 200
99
+ grading:
100
+ throughput_weight: 0.60
101
+ efficiency_weight: 0.40
102
+ target_throughput_per_step: 1.8
103
+
104
+ - id: emergency_priority
105
+ name: Emergency Vehicle Prioritisation
106
+ difficulty: medium
107
+ description: >
108
+ Manage mixed traffic while prioritising occasional emergency vehicles
109
+ that arrive from random directions with high urgency.
110
+ max_steps: 300
111
+ grading:
112
+ throughput_weight: 0.30
113
+ emergency_weight: 0.35
114
+ delay_weight: 0.20
115
+ efficiency_weight: 0.15
116
+ target_emergency_delay_steps: 3
117
+
118
+ - id: dynamic_scenarios
119
+ name: Dynamic and Complex Scenarios
120
+ difficulty: hard
121
+ description: >
122
+ Handle high traffic density, traffic-surge events, and multiple
123
+ simultaneous emergency vehicles. Robustness and collision avoidance
124
+ are critical evaluation criteria.
125
+ max_steps: 400
126
+ grading:
127
+ throughput_weight: 0.25
128
+ emergency_weight: 0.30
129
+ delay_weight: 0.20
130
+ efficiency_weight: 0.15
131
+ adaptability_weight: 0.10
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Server
135
+ # ---------------------------------------------------------------------------
136
+ server:
137
+ port: 8000
138
+ module: traffic_control.server.app
139
+ app: app
140
+ workers: 2
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Docker
144
+ # ---------------------------------------------------------------------------
145
+ docker:
146
+ base_image: python:3.11-slim
147
+ exposed_port: 8000
openenv_traffic_control.egg-info/PKG-INFO ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-traffic-control
3
+ Version: 1.0.0
4
+ Summary: Autonomous Traffic Control RL Environment – OpenEnv Hackathon
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: openenv-core[core]>=0.2.2
9
+ Requires-Dist: openai>=1.0.0
10
+ Requires-Dist: gradio>=4.0.0
11
+ Requires-Dist: numpy>=1.24.0
12
+ Requires-Dist: python-dotenv>=1.0.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
15
+ Requires-Dist: httpx>=0.25.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
17
+
18
+ ---
19
+ title: Traffic Control Environment Server
20
+ emoji: 🎯
21
+ colorFrom: gray
22
+ colorTo: gray
23
+ sdk: docker
24
+ pinned: false
25
+ app_port: 8000
26
+ base_path: /web
27
+ tags:
28
+ - openenv
29
+ ---
30
+
31
+ # Traffic Control Environment
32
+
33
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
34
+
35
+ ## Quick Start
36
+
37
+ The simplest way to use the Traffic Control environment is through the `TrafficControlEnv` class:
38
+
39
+ ```python
40
+ from traffic_control import TrafficControlAction, TrafficControlEnv
41
+
42
+ try:
43
+ # Create environment from Docker image
44
+ traffic_controlenv = TrafficControlEnv.from_docker_image("traffic_control-env:latest")
45
+
46
+ # Reset
47
+ result = traffic_controlenv.reset()
48
+ print(f"Reset: {result.observation.echoed_message}")
49
+
50
+ # Send multiple messages
51
+ messages = ["Hello, World!", "Testing echo", "Final message"]
52
+
53
+ for msg in messages:
54
+ result = traffic_controlenv.step(TrafficControlAction(message=msg))
55
+ print(f"Sent: '{msg}'")
56
+ print(f" → Echoed: '{result.observation.echoed_message}'")
57
+ print(f" → Length: {result.observation.message_length}")
58
+ print(f" → Reward: {result.reward}")
59
+
60
+ finally:
61
+ # Always clean up
62
+ traffic_controlenv.close()
63
+ ```
64
+
65
+ That's it! The `TrafficControlEnv.from_docker_image()` method handles:
66
+ - Starting the Docker container
67
+ - Waiting for the server to be ready
68
+ - Connecting to the environment
69
+ - Container cleanup when you call `close()`
70
+
71
+ ## Building the Docker Image
72
+
73
+ Before using the environment, you need to build the Docker image:
74
+
75
+ ```bash
76
+ # From project root
77
+ docker build -t traffic_control-env:latest -f server/Dockerfile .
78
+ ```
79
+
80
+ ## Deploying to Hugging Face Spaces
81
+
82
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
83
+
84
+ ```bash
85
+ # From the environment directory (where openenv.yaml is located)
86
+ openenv push
87
+
88
+ # Or specify options
89
+ openenv push --namespace my-org --private
90
+ ```
91
+
92
+ The `openenv push` command will:
93
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
94
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
95
+ 3. Upload to Hugging Face (ensuring you're logged in)
96
+
97
+ ### Prerequisites
98
+
99
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
100
+
101
+ ### Options
102
+
103
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
104
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
105
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
106
+ - `--private`: Deploy the space as private (default: public)
107
+
108
+ ### Examples
109
+
110
+ ```bash
111
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
112
+ openenv push
113
+
114
+ # Push to a specific repository
115
+ openenv push --repo-id my-org/my-env
116
+
117
+ # Push with a custom base image
118
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
119
+
120
+ # Push as a private space
121
+ openenv push --private
122
+
123
+ # Combine options
124
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
125
+ ```
126
+
127
+ After deployment, your space will be available at:
128
+ `https://huggingface.co/spaces/<repo-id>`
129
+
130
+ The deployed space includes:
131
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
132
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
133
+ - **Health Check** at `/health` - Container health monitoring
134
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
135
+
136
+ ## Environment Details
137
+
138
+ ### Action
139
+ **TrafficControlAction**: Contains a single field
140
+ - `message` (str) - The message to echo back
141
+
142
+ ### Observation
143
+ **TrafficControlObservation**: Contains the echo response and metadata
144
+ - `echoed_message` (str) - The message echoed back
145
+ - `message_length` (int) - Length of the message
146
+ - `reward` (float) - Reward based on message length (length × 0.1)
147
+ - `done` (bool) - Always False for echo environment
148
+ - `metadata` (dict) - Additional info like step count
149
+
150
+ ### Reward
151
+ The reward is calculated as: `message_length × 0.1`
152
+ - "Hi" → reward: 0.2
153
+ - "Hello, World!" → reward: 1.3
154
+ - Empty message → reward: 0.0
155
+
156
+ ## Advanced Usage
157
+
158
+ ### Connecting to an Existing Server
159
+
160
+ If you already have a Traffic Control environment server running, you can connect directly:
161
+
162
+ ```python
163
+ from traffic_control import TrafficControlEnv
164
+
165
+ # Connect to existing server
166
+ traffic_controlenv = TrafficControlEnv(base_url="<ENV_HTTP_URL_HERE>")
167
+
168
+ # Use as normal
169
+ result = traffic_controlenv.reset()
170
+ result = traffic_controlenv.step(TrafficControlAction(message="Hello!"))
171
+ ```
172
+
173
+ Note: When connecting to an existing server, `traffic_controlenv.close()` will NOT stop the server.
174
+
175
+ ### Using the Context Manager
176
+
177
+ The client supports context manager usage for automatic connection management:
178
+
179
+ ```python
180
+ from traffic_control import TrafficControlAction, TrafficControlEnv
181
+
182
+ # Connect with context manager (auto-connects and closes)
183
+ with TrafficControlEnv(base_url="http://localhost:8000") as env:
184
+ result = env.reset()
185
+ print(f"Reset: {result.observation.echoed_message}")
186
+ # Multiple steps with low latency
187
+ for msg in ["Hello", "World", "!"]:
188
+ result = env.step(TrafficControlAction(message=msg))
189
+ print(f"Echoed: {result.observation.echoed_message}")
190
+ ```
191
+
192
+ The client uses WebSocket connections for:
193
+ - **Lower latency**: No HTTP connection overhead per request
194
+ - **Persistent session**: Server maintains your environment state
195
+ - **Efficient for episodes**: Better for many sequential steps
196
+
197
+ ### Concurrent WebSocket Sessions
198
+
199
+ The server supports multiple concurrent WebSocket connections. To enable this,
200
+ modify `server/app.py` to use factory mode:
201
+
202
+ ```python
203
+ # In server/app.py - use factory mode for concurrent sessions
204
+ app = create_app(
205
+ TrafficControlEnvironment, # Pass class, not instance
206
+ TrafficControlAction,
207
+ TrafficControlObservation,
208
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
209
+ )
210
+ ```
211
+
212
+ Then multiple clients can connect simultaneously:
213
+
214
+ ```python
215
+ from traffic_control import TrafficControlAction, TrafficControlEnv
216
+ from concurrent.futures import ThreadPoolExecutor
217
+
218
+ def run_episode(client_id: int):
219
+ with TrafficControlEnv(base_url="http://localhost:8000") as env:
220
+ result = env.reset()
221
+ for i in range(10):
222
+ result = env.step(TrafficControlAction(message=f"Client {client_id}, step {i}"))
223
+ return client_id, result.observation.message_length
224
+
225
+ # Run 4 episodes concurrently
226
+ with ThreadPoolExecutor(max_workers=4) as executor:
227
+ results = list(executor.map(run_episode, range(4)))
228
+ ```
229
+
230
+ ## Development & Testing
231
+
232
+ ### Direct Environment Testing
233
+
234
+ Test the environment logic directly without starting the HTTP server:
235
+
236
+ ```bash
237
+ # From the server directory
238
+ python3 server/traffic_control_environment.py
239
+ ```
240
+
241
+ This verifies that:
242
+ - Environment resets correctly
243
+ - Step executes actions properly
244
+ - State tracking works
245
+ - Rewards are calculated correctly
246
+
247
+ ### Running Locally
248
+
249
+ Run the server locally for development:
250
+
251
+ ```bash
252
+ uvicorn server.app:app --reload
253
+ ```
254
+
255
+ ## Project Structure
256
+
257
+ ```
258
+ traffic_control/
259
+ ├── .dockerignore # Docker build exclusions
260
+ ├── __init__.py # Module exports
261
+ ├── README.md # This file
262
+ ├── openenv.yaml # OpenEnv manifest
263
+ ├── pyproject.toml # Project metadata and dependencies
264
+ ├── uv.lock # Locked dependencies (generated)
265
+ ├── client.py # TrafficControlEnv client
266
+ ├── models.py # Action and Observation models
267
+ └── server/
268
+ ├── __init__.py # Server module exports
269
+ ├── traffic_control_environment.py # Core environment logic
270
+ ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
271
+ └── Dockerfile # Container image definition
272
+ ```
openenv_traffic_control.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ pyproject.toml
3
+ ./__init__.py
4
+ ./client.py
5
+ ./models.py
6
+ openenv_traffic_control.egg-info/PKG-INFO
7
+ openenv_traffic_control.egg-info/SOURCES.txt
8
+ openenv_traffic_control.egg-info/dependency_links.txt
9
+ openenv_traffic_control.egg-info/entry_points.txt
10
+ openenv_traffic_control.egg-info/requires.txt
11
+ openenv_traffic_control.egg-info/top_level.txt
12
+ server/__init__.py
13
+ server/app.py
14
+ server/traffic_control_environment.py
openenv_traffic_control.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
openenv_traffic_control.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = traffic_control.server.app:main
openenv_traffic_control.egg-info/requires.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.2
2
+ openai>=1.0.0
3
+ gradio>=4.0.0
4
+ numpy>=1.24.0
5
+ python-dotenv>=1.0.0
6
+
7
+ [dev]
8
+ pytest>=7.4.0
9
+ httpx>=0.25.0
10
+ pytest-asyncio>=0.23.0
openenv_traffic_control.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ traffic_control
pyproject.toml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openenv-traffic-control"
7
+ version = "1.0.0"
8
+ description = "Autonomous Traffic Control RL Environment - OpenEnv Hackathon"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "openenv-core[core]>=0.2.2",
14
+ "openai>=1.0.0",
15
+ "gradio>=4.0.0",
16
+ "numpy>=1.24.0",
17
+ "python-dotenv>=1.0.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=7.4.0",
23
+ "httpx>=0.25.0",
24
+ "pytest-asyncio>=0.23.0",
25
+ ]
26
+
27
+ [project.scripts]
28
+ server = "traffic_control.server.app:main"
29
+
30
+ # ── Setuptools config ──────────────────────────────────────────────────────
31
+ # pyproject.toml lives in openv/traffic_control/
32
+ # The Python package traffic_control/ lives one level UP at openv/
33
+ # so we point package-dir to the parent.
34
+ [tool.setuptools]
35
+ include-package-data = true
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = [".."]
39
+ include = ["traffic_control*"]
server/Dockerfile ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=traffic_control
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
server/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Server package for the Traffic Control environment."""
2
+
3
+ from .app import app, main
4
+
5
+ __all__ = ["app", "main"]
server/app.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI app for the Autonomous Traffic Control OpenEnv environment.
3
+
4
+ Endpoints provided automatically by openenv-core create_app():
5
+ POST /reset – start a new episode
6
+ POST /step – execute one action
7
+ GET /state – episode-level cumulative state
8
+ GET /schema – action / observation JSON schemas
9
+ WS /ws – WebSocket for persistent sessions
10
+ GET /health – liveness probe
11
+ GET /docs – Swagger UI
12
+
13
+ Custom endpoints added here:
14
+ POST /grade – run the automated task grader (returns 0-1 score)
15
+ GET /ui – Gradio testing interface
16
+
17
+ Usage:
18
+ # From traffic_control/ directory:
19
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --reload
20
+ python -m traffic_control.server.app
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import sys
26
+ import os
27
+
28
+ # Ensure this package's parent is on sys.path so relative package imports work
29
+ # regardless of from where uvicorn is invoked.
30
+ _PKG_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # traffic_control/
31
+ _ROOT = os.path.dirname(_PKG_DIR) # openv/
32
+ for _p in (_PKG_DIR, _ROOT):
33
+ if _p not in sys.path:
34
+ sys.path.insert(0, _p)
35
+
36
+ from openenv.core.env_server.http_server import create_app
37
+ from fastapi import Request
38
+
39
+ # All imports from within traffic_control/ only
40
+ from traffic_control.models import TrafficAction, TrafficObservation
41
+ from traffic_control.environment import TrafficControlEnvironment
42
+ from traffic_control.tasks import grade as run_grader
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # 1. Standard OpenEnv app
47
+ # ---------------------------------------------------------------------------
48
+
49
+ app = create_app(
50
+ TrafficControlEnvironment,
51
+ TrafficAction,
52
+ TrafficObservation,
53
+ env_name="traffic_control",
54
+ max_concurrent_envs=4,
55
+ )
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # 2. /grade endpoint
60
+ # ---------------------------------------------------------------------------
61
+
62
+ @app.post("/grade", tags=["eval"])
63
+ async def grade(request: Request):
64
+ """
65
+ Run the automated grader for the environment's current state.
66
+
67
+ Body (all optional):
68
+ task_id, total_vehicles_passed, total_emergency_passed,
69
+ total_waiting_time, total_collisions, total_emergency_delay,
70
+ total_phase_changes, step_count
71
+ """
72
+ body = {}
73
+ try:
74
+ body = await request.json()
75
+ except Exception:
76
+ pass
77
+
78
+ task_id = body.get("task_id", "basic_flow")
79
+ total_vehicles = int(body.get("total_vehicles_passed", 0))
80
+ total_emergency = int(body.get("total_emergency_passed", 0))
81
+ total_waiting = float(body.get("total_waiting_time", 0.0))
82
+ total_collisions = int(body.get("total_collisions", 0))
83
+ total_emergency_delay = float(body.get("total_emergency_delay", 0.0))
84
+ total_phase_changes = int(body.get("total_phase_changes", 0))
85
+ step_count = int(body.get("step_count", 1))
86
+
87
+ result = run_grader(
88
+ task_id,
89
+ total_vehicles_passed=total_vehicles,
90
+ total_emergency_passed=total_emergency,
91
+ total_waiting_time=total_waiting,
92
+ total_collisions=total_collisions,
93
+ total_emergency_delay=total_emergency_delay,
94
+ total_phase_changes=total_phase_changes,
95
+ step_count=step_count,
96
+ )
97
+
98
+ return {
99
+ "task_id": task_id,
100
+ "score": result.score,
101
+ "metrics": result.metrics,
102
+ "feedback": result.feedback,
103
+ }
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # 3. Gradio UI (mounted at /ui)
108
+ # ---------------------------------------------------------------------------
109
+
110
+ try:
111
+ import gradio as gr
112
+ import requests as _req
113
+
114
+ # Use SERVER_PORT env var (default 8000) so the UI works on any port
115
+ _PORT = int(os.environ.get("PORT", os.environ.get("SERVER_PORT", "8000")))
116
+ _SELF_BASE = f"http://127.0.0.1:{_PORT}"
117
+
118
+ def _reset_env(task_id: str):
119
+ try:
120
+ r = _req.post(
121
+ f"{_SELF_BASE}/reset",
122
+ json={"task_id": task_id, "seed": 42},
123
+ timeout=10,
124
+ )
125
+ return r.json() if r.status_code == 200 else {"error": r.text}
126
+ except Exception as exc:
127
+ return {"error": str(exc)}
128
+
129
+ def _step_env(phase: str):
130
+ try:
131
+ # openenv-core wraps the action under an "action" key
132
+ r = _req.post(
133
+ f"{_SELF_BASE}/step",
134
+ json={"action": {"light_phase": int(phase)}},
135
+ timeout=10,
136
+ )
137
+ return r.json() if r.status_code == 200 else {"error": r.text}
138
+ except Exception as exc:
139
+ return {"error": str(exc)}
140
+
141
+ def _get_state():
142
+ try:
143
+ r = _req.get(f"{_SELF_BASE}/state", timeout=10)
144
+ return r.json() if r.status_code == 200 else {"error": r.text}
145
+ except Exception as exc:
146
+ return {"error": str(exc)}
147
+
148
+ with gr.Blocks(title="Traffic Control UI") as _ui:
149
+ gr.Markdown("# 🚦 Autonomous Traffic Control — Testing UI")
150
+ gr.Markdown("Interact with the OpenEnv HTTP API live.")
151
+
152
+ with gr.Row():
153
+ _task = gr.Dropdown(
154
+ choices=["basic_flow", "emergency_priority", "dynamic_scenarios"],
155
+ value="basic_flow",
156
+ label="Task ID",
157
+ )
158
+ _reset_btn = gr.Button("🔄 Reset")
159
+ _state_btn = gr.Button("📊 State")
160
+
161
+ with gr.Row():
162
+ _phase = gr.Radio(
163
+ choices=[("0 – NS Green", "0"), ("1 – EW Green", "1"), ("2 – All Red", "2")],
164
+ value="0",
165
+ label="Next Action (Light Phase)",
166
+ )
167
+ _step_btn = gr.Button("▶️ Step", variant="primary")
168
+
169
+ _out = gr.JSON(label="API Response")
170
+
171
+ _reset_btn.click(_reset_env, inputs=[_task], outputs=[_out])
172
+ _step_btn.click(_step_env, inputs=[_phase], outputs=[_out])
173
+ _state_btn.click(_get_state, outputs=[_out])
174
+
175
+ app = gr.mount_gradio_app(app, _ui, path="/ui")
176
+
177
+ except ImportError:
178
+ # Gradio is optional; server still works without it
179
+ pass
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Entry point
184
+ # ---------------------------------------------------------------------------
185
+
186
+ def main() -> None:
187
+ """Entry point: start uvicorn server. Reads --host and --port from CLI args."""
188
+ import argparse
189
+ import uvicorn
190
+
191
+ parser = argparse.ArgumentParser(description="Traffic Control OpenEnv Server")
192
+ parser.add_argument("--host", default=os.environ.get("HOST", "0.0.0.0"))
193
+ parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "8000")))
194
+ parser.add_argument("--workers", type=int, default=1)
195
+ args, _ = parser.parse_known_args() # ignore unknown args from uv
196
+
197
+ uvicorn.run(
198
+ "traffic_control.server.app:app",
199
+ host=args.host,
200
+ port=args.port,
201
+ workers=args.workers,
202
+ )
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openenv[core]>=0.2.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+
5
+
6
+
server/traffic_control_environment.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Traffic Control Environment Implementation.
9
+
10
+ A simple test environment that echoes back messages sent to it.
11
+ Perfect for testing HTTP server infrastructure.
12
+ """
13
+
14
+ from uuid import uuid4
15
+
16
+ from openenv.core.env_server.interfaces import Environment
17
+ from openenv.core.env_server.types import State
18
+
19
+ try:
20
+ from ..models import TrafficControlAction, TrafficControlObservation
21
+ except ImportError:
22
+ from models import TrafficControlAction, TrafficControlObservation
23
+
24
+
25
+ class TrafficControlEnvironment(Environment):
26
+ """
27
+ A simple echo environment that echoes back messages.
28
+
29
+ This environment is designed for testing the HTTP server infrastructure.
30
+ It maintains minimal state and simply echoes back whatever message it receives.
31
+
32
+ Example:
33
+ >>> env = TrafficControlEnvironment()
34
+ >>> obs = env.reset()
35
+ >>> print(obs.echoed_message) # "Traffic Control environment ready!"
36
+ >>>
37
+ >>> obs = env.step(TrafficControlAction(message="Hello"))
38
+ >>> print(obs.echoed_message) # "Hello"
39
+ >>> print(obs.message_length) # 5
40
+ """
41
+
42
+ # Enable concurrent WebSocket sessions.
43
+ # Set to True if your environment isolates state between instances.
44
+ # When True, multiple WebSocket clients can connect simultaneously, each
45
+ # getting their own environment instance (when using factory mode in app.py).
46
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
47
+
48
+ def __init__(self):
49
+ """Initialize the traffic_control environment."""
50
+ self._state = State(episode_id=str(uuid4()), step_count=0)
51
+ self._reset_count = 0
52
+
53
+ def reset(self) -> TrafficControlObservation:
54
+ """
55
+ Reset the environment.
56
+
57
+ Returns:
58
+ TrafficControlObservation with a ready message
59
+ """
60
+ self._state = State(episode_id=str(uuid4()), step_count=0)
61
+ self._reset_count += 1
62
+
63
+ return TrafficControlObservation(
64
+ echoed_message="Traffic Control environment ready!",
65
+ message_length=0,
66
+ done=False,
67
+ reward=0.0,
68
+ )
69
+
70
+ def step(self, action: TrafficControlAction) -> TrafficControlObservation: # type: ignore[override]
71
+ """
72
+ Execute a step in the environment by echoing the message.
73
+
74
+ Args:
75
+ action: TrafficControlAction containing the message to echo
76
+
77
+ Returns:
78
+ TrafficControlObservation with the echoed message and its length
79
+ """
80
+ self._state.step_count += 1
81
+
82
+ message = action.message
83
+ length = len(message)
84
+
85
+ # Simple reward: longer messages get higher rewards
86
+ reward = length * 0.1
87
+
88
+ return TrafficControlObservation(
89
+ echoed_message=message,
90
+ message_length=length,
91
+ done=False,
92
+ reward=reward,
93
+ metadata={"original_message": message, "step": self._state.step_count},
94
+ )
95
+
96
+ @property
97
+ def state(self) -> State:
98
+ """
99
+ Get the current environment state.
100
+
101
+ Returns:
102
+ Current State with episode_id and step_count
103
+ """
104
+ return self._state
tasks.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Task graders for the Autonomous Traffic Control OpenEnv environment.
3
+
4
+ Defines three tasks of increasing difficulty:
5
+ 1. basic_flow – baseline throughput optimisation (Easy)
6
+ 2. emergency_priority – emergency vehicle management + throughput (Medium)
7
+ 3. dynamic_scenarios – surge-traffic + emergencies under hard constraints (Hard)
8
+
9
+ Each grader returns a GradeResult(score, metrics, feedback) with 0–1 score.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Dict
16
+
17
+
18
+ @dataclass
19
+ class GradeResult:
20
+ """Standardised grading result."""
21
+ score: float # strictly in (0.001, 0.999)
22
+ metrics: Dict[str, Any] = field(default_factory=dict)
23
+ feedback: str = ""
24
+
25
+
26
+ def _clamp(score: float) -> float:
27
+ """Ensure score is strictly between 0 and 1 (never 0.0 or 1.0 exactly)."""
28
+ return round(max(0.001, min(0.999, score)), 4)
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Public entry point
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def grade(
36
+ task_id: str,
37
+ *,
38
+ total_vehicles_passed: int = 0,
39
+ total_emergency_passed: int = 0,
40
+ total_waiting_time: float = 0.0,
41
+ total_collisions: int = 0,
42
+ total_emergency_delay: float = 0.0,
43
+ total_phase_changes: int = 0,
44
+ step_count: int = 1,
45
+ ) -> GradeResult:
46
+ """Route to the appropriate task grader."""
47
+ graders = {
48
+ "basic_flow": _grade_basic_flow,
49
+ "emergency_priority": _grade_emergency_priority,
50
+ "dynamic_scenarios": _grade_dynamic_scenarios,
51
+ }
52
+ if task_id not in graders:
53
+ return GradeResult(
54
+ score=0.001,
55
+ feedback=f"Unknown task_id '{task_id}'. Valid: {list(graders.keys())}",
56
+ )
57
+ return graders[task_id](
58
+ total_vehicles_passed=total_vehicles_passed,
59
+ total_emergency_passed=total_emergency_passed,
60
+ total_waiting_time=total_waiting_time,
61
+ total_collisions=total_collisions,
62
+ total_emergency_delay=total_emergency_delay,
63
+ total_phase_changes=total_phase_changes,
64
+ step_count=max(step_count, 1),
65
+ )
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Task 1 – Basic Flow (Easy)
70
+ # ---------------------------------------------------------------------------
71
+
72
+ _BASIC_FLOW_TARGET_THROUGHPUT_PER_STEP = 1.8 # vehicles/step considered "perfect"
73
+
74
+
75
+ def _grade_basic_flow(
76
+ *,
77
+ total_vehicles_passed: int,
78
+ total_waiting_time: float,
79
+ total_collisions: int,
80
+ step_count: int,
81
+ **_ignored,
82
+ ) -> GradeResult:
83
+ throughput_per_step = total_vehicles_passed / step_count
84
+ throughput_score = min(throughput_per_step / _BASIC_FLOW_TARGET_THROUGHPUT_PER_STEP, 1.0)
85
+ efficiency_score = 1.0 / (1.0 + total_waiting_time / max(step_count, 1) * 0.1)
86
+ collision_penalty = 0.8 if total_collisions > 0 else 0.0
87
+
88
+ raw = throughput_score * 0.6 + efficiency_score * 0.4
89
+ score = max(0.0, raw - collision_penalty)
90
+
91
+ return GradeResult(
92
+ score=_clamp(raw - collision_penalty),
93
+ metrics={
94
+ "throughput_per_step": round(throughput_per_step, 3),
95
+ "throughput_score": round(throughput_score, 4),
96
+ "efficiency_score": round(efficiency_score, 4),
97
+ "total_collisions": total_collisions,
98
+ "collision_penalty": collision_penalty,
99
+ },
100
+ feedback=(
101
+ f"Throughput {throughput_per_step:.2f} veh/step "
102
+ f"(target {_BASIC_FLOW_TARGET_THROUGHPUT_PER_STEP}). "
103
+ + ("⚠ Collision penalty applied!" if total_collisions else "No collisions ✓.")
104
+ ),
105
+ )
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Task 2 – Emergency Priority (Medium)
110
+ # ---------------------------------------------------------------------------
111
+
112
+ _EMERG_TARGET_DELAY_PER_VEHICLE = 3.0 # steps/emergency vehicle
113
+
114
+
115
+ def _grade_emergency_priority(
116
+ *,
117
+ total_vehicles_passed: int,
118
+ total_emergency_passed: int,
119
+ total_waiting_time: float,
120
+ total_collisions: int,
121
+ total_emergency_delay: float,
122
+ step_count: int,
123
+ **_ignored,
124
+ ) -> GradeResult:
125
+ throughput_per_step = total_vehicles_passed / step_count
126
+ throughput_score = min(throughput_per_step / 1.5, 1.0)
127
+
128
+ # Emergency throughput score: 1.0 if ≥ 1 emergency vehicle cleared per 20 steps
129
+ em_rate = total_emergency_passed / step_count
130
+ em_rate_score = min(em_rate / (1.0 / 20.0), 1.0)
131
+
132
+ # Emergency delay score
133
+ if total_emergency_passed > 0:
134
+ avg_delay = total_emergency_delay / total_emergency_passed
135
+ delay_score = max(0.0, 1.0 - avg_delay / (_EMERG_TARGET_DELAY_PER_VEHICLE * 4))
136
+ else:
137
+ delay_score = 0.5
138
+
139
+ efficiency_score = 1.0 / (1.0 + total_waiting_time / max(step_count, 1) * 0.05)
140
+ collision_penalty = 0.85 if total_collisions > 0 else 0.0
141
+
142
+ raw = (throughput_score * 0.30 + em_rate_score * 0.35 +
143
+ delay_score * 0.20 + efficiency_score * 0.15)
144
+ score = max(0.0, raw - collision_penalty)
145
+
146
+ avg_delay_str = (
147
+ f"{total_emergency_delay / total_emergency_passed:.1f} steps"
148
+ if total_emergency_passed else "N/A"
149
+ )
150
+
151
+ return GradeResult(
152
+ score=_clamp(raw - collision_penalty),
153
+ metrics={
154
+ "throughput_per_step": round(throughput_per_step, 3),
155
+ "throughput_score": round(throughput_score, 4),
156
+ "emergency_rate_score": round(em_rate_score, 4),
157
+ "emergency_delay_score": round(delay_score, 4),
158
+ "efficiency_score": round(efficiency_score, 4),
159
+ "total_emergency_passed": total_emergency_passed,
160
+ "avg_emergency_delay_steps": avg_delay_str,
161
+ "total_collisions": total_collisions,
162
+ },
163
+ feedback=(
164
+ f"Cleared {total_emergency_passed} emergency vehicles "
165
+ f"(avg delay {avg_delay_str}). "
166
+ f"Throughput {throughput_per_step:.2f} veh/step. "
167
+ + ("⚠ Collision!" if total_collisions else "No collisions ✓.")
168
+ ),
169
+ )
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Task 3 – Dynamic Scenarios (Hard)
174
+ # ---------------------------------------------------------------------------
175
+
176
+ def _grade_dynamic_scenarios(
177
+ *,
178
+ total_vehicles_passed: int,
179
+ total_emergency_passed: int,
180
+ total_waiting_time: float,
181
+ total_collisions: int,
182
+ total_emergency_delay: float,
183
+ total_phase_changes: int,
184
+ step_count: int,
185
+ **_ignored,
186
+ ) -> GradeResult:
187
+ throughput_per_step = total_vehicles_passed / step_count
188
+ throughput_score = min(throughput_per_step / 2.0, 1.0)
189
+
190
+ em_rate = total_emergency_passed / step_count
191
+ em_rate_score = min(em_rate / (1.0 / 15.0), 1.0)
192
+
193
+ if total_emergency_passed > 0:
194
+ avg_delay = total_emergency_delay / total_emergency_passed
195
+ delay_score = max(0.0, 1.0 - avg_delay / 5.0)
196
+ else:
197
+ delay_score = 0.0
198
+
199
+ efficiency_score = 1.0 / (1.0 + total_waiting_time / max(step_count, 1) * 0.08)
200
+ adaptability_score = 1.0 / (1.0 + total_phase_changes / max(step_count, 1) * 0.5)
201
+ collision_penalty = 0.9 if total_collisions > 0 else 0.0
202
+
203
+ raw = (throughput_score * 0.25 + em_rate_score * 0.30 +
204
+ delay_score * 0.20 + efficiency_score * 0.15 +
205
+ adaptability_score * 0.10)
206
+ score = max(0.0, raw - collision_penalty)
207
+
208
+ return GradeResult(
209
+ score=_clamp(raw - collision_penalty),
210
+ metrics={
211
+ "throughput_per_step": round(throughput_per_step, 3),
212
+ "throughput_score": round(throughput_score, 4),
213
+ "emergency_rate_score": round(em_rate_score, 4),
214
+ "emergency_delay_score": round(delay_score, 4),
215
+ "efficiency_score": round(efficiency_score, 4),
216
+ "adaptability_score": round(adaptability_score, 4),
217
+ "total_collisions": total_collisions,
218
+ "total_phase_changes": total_phase_changes,
219
+ },
220
+ feedback=(
221
+ f"Dynamic task: throughput {throughput_per_step:.2f} veh/step, "
222
+ f"{total_emergency_passed} emergencies cleared, "
223
+ f"{total_phase_changes} phase changes over {step_count} steps. "
224
+ + ("⚠ Collision!" if total_collisions else "No collisions ✓.")
225
+ ),
226
+ )
uv.lock ADDED
The diff for this file is too large to render. See raw diff