Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +61 -0
- README.md +249 -4
- __init__.py +13 -0
- client.py +45 -0
- environment.py +438 -0
- inference.py +259 -0
- models.py +120 -0
- openenv.yaml +147 -0
- openenv_traffic_control.egg-info/PKG-INFO +272 -0
- openenv_traffic_control.egg-info/SOURCES.txt +14 -0
- openenv_traffic_control.egg-info/dependency_links.txt +1 -0
- openenv_traffic_control.egg-info/entry_points.txt +2 -0
- openenv_traffic_control.egg-info/requires.txt +10 -0
- openenv_traffic_control.egg-info/top_level.txt +1 -0
- pyproject.toml +39 -0
- server/Dockerfile +80 -0
- server/__init__.py +5 -0
- server/app.py +206 -0
- server/requirements.txt +6 -0
- server/traffic_control_environment.py +104 -0
- tasks.py +226 -0
- uv.lock +0 -0
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:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: gray
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|