amanmurari commited on
Commit
df3ceb8
·
verified ·
1 Parent(s): 674a510

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. inference.py +147 -340
inference.py CHANGED
@@ -4,13 +4,11 @@ import os
4
  import sys
5
  import json
6
  import time
7
- import math
8
  import urllib.request
9
  from collections import deque
10
  from typing import Deque, List, Optional, Tuple
11
 
12
- # Allow imports from repo root so both container-root and package contexts work
13
- _HERE = os.path.dirname(os.path.abspath(__file__))
14
  _PARENT = os.path.dirname(_HERE)
15
  for _p in (_HERE, _PARENT):
16
  if _p not in sys.path:
@@ -26,352 +24,183 @@ except ImportError:
26
  from models import TrafficAction, TrafficObservation, TrafficState # type: ignore
27
 
28
  # ---------------------------------------------------------------------------
29
- # Environment variables — injected by hackathon validator
30
  # ---------------------------------------------------------------------------
31
  API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
32
  API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
33
  MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
34
  SERVER_URL = os.getenv("SERVER_URL", "http://localhost:7860")
35
  SEED = 42
36
- MAX_TOKENS = 200 # shorter = faster response
37
- TEMPERATURE = 0.0
38
- LLM_TIMEOUT_S = 12 # per-call timeout (seconds)
39
- LLM_CALL_EVERY = 5 # only call LLM every N steps (heuristic fills the rest)
40
- TASK_BUDGET_S = { # hard wall-clock budget per task (seconds)
41
- "basic_flow": 480,
42
- "emergency_priority": 720,
43
- "dynamic_scenarios": 960,
 
 
 
44
  }
45
 
46
- # Task max_steps for score projection
47
  TASK_MAX_STEPS = {"basic_flow": 200, "emergency_priority": 300, "dynamic_scenarios": 400}
48
 
49
- # Task-specific minimum green hold (steps before switching is even considered).
50
- # basic_flow: stability bonus requires low switch rate — hold longer.
51
- # emergency tasks: react fast to emergencies — hold shorter.
52
- MIN_HOLD_BY_TASK = {
53
- "basic_flow": 6,
54
- "emergency_priority": 3,
55
- "dynamic_scenarios": 3,
56
- }
57
- DEFAULT_MIN_HOLD = 4
58
-
59
- # Pressure ratio required to justify a switch (avoid switching-penalty)
60
- # Higher for basic_flow (graded on stability), lower for emergency tasks
61
- SWITCH_RATIO_BY_TASK = {
62
- "basic_flow": 1.5,
63
- "emergency_priority": 1.2,
64
- "dynamic_scenarios": 1.2,
65
- }
66
 
67
  # ---------------------------------------------------------------------------
68
- # Heuristic coreuses actual environment reward formula
69
  # ---------------------------------------------------------------------------
70
 
71
  def _em_pressure(count: int, urgency: int) -> float:
72
- """Emergency pressure = count × urgency^1.5 × 0.5 (matches env._compute_reward)."""
73
  return count * (max(urgency, 1) ** 1.5) * 0.5 if count > 0 else 0.0
74
 
75
-
76
  def _dir_pressure(queue: int, em_count: int, urgency: int) -> float:
77
- """Combined directional pressure: throughput value + weighted emergency penalty."""
78
  return queue * 0.30 + _em_pressure(em_count, urgency) * 4.0
79
 
80
-
81
  def _compute_pressures(obs: TrafficObservation) -> Tuple[float, float]:
82
- """Return (ns_pressure, ew_pressure)."""
83
- ns_p = _dir_pressure(
84
  obs.queue_lengths[0] + obs.queue_lengths[1],
85
  obs.emergency_queue[0] + obs.emergency_queue[1],
86
  max(obs.emergency_urgency[0], obs.emergency_urgency[1]),
87
  )
88
- ew_p = _dir_pressure(
89
  obs.queue_lengths[2] + obs.queue_lengths[3],
90
  obs.emergency_queue[2] + obs.emergency_queue[3],
91
  max(obs.emergency_urgency[2], obs.emergency_urgency[3]),
92
  )
93
- return ns_p, ew_p
94
-
95
-
96
- def _dynamic_hold_time(obs: TrafficObservation, task: str) -> int:
97
- """
98
- Adaptive minimum hold: longer when current direction has more traffic,
99
- scaled by task type (basic_flow needs more stability).
100
- """
101
- base = MIN_HOLD_BY_TASK.get(task, DEFAULT_MIN_HOLD)
102
- cur = obs.current_phase
103
- if cur in (0, 3): # NS_GREEN / NS_YELLOW
104
- q = obs.queue_lengths[0] + obs.queue_lengths[1]
105
- elif cur in (1, 4): # EW_GREEN / EW_YELLOW
106
- q = obs.queue_lengths[2] + obs.queue_lengths[3]
107
- else:
108
- return base
109
- # Hold longer if queue is deep (drain rate ~3 veh/step), cap at 12
110
- return max(base, min(q // 3, 12))
111
-
112
-
113
- def _collision_risk(obs: TrafficObservation) -> bool:
114
- """
115
- Detect gridlock risk early (environment triggers -200 at total_queued>40 AND time>20).
116
- We act at 70% of threshold so we can clear before the penalty triggers.
117
- """
118
- total_q = sum(obs.queue_lengths)
119
- return total_q > 28 and obs.time_in_phase > 14
120
-
121
-
122
- def _heuristic_phase(obs: TrafficObservation, task: str) -> Tuple[int, str]:
123
- """
124
- Mathematically optimal phase recommendation.
125
- Returns (phase, reason) so caller can log it.
126
-
127
- Priority order:
128
- 1. Collision risk — proactively rotate to drain largest queue
129
- 2. Critical emergency (urgency ≥ 8) — clear NOW
130
- 3. Pre-emptive emergency (urgency 6-7) — clear before escalation
131
- 4. Moderate emergency (urgency 5)
132
- 5. Hysteresis — don't switch if hold time not reached
133
- 6. Queue pressure — switch to higher pressure direction
134
- 7. Default — hold current
135
- """
136
  ns_em = obs.emergency_queue[0] + obs.emergency_queue[1]
137
  ew_em = obs.emergency_queue[2] + obs.emergency_queue[3]
138
  ns_urg = max(obs.emergency_urgency[0], obs.emergency_urgency[1])
139
  ew_urg = max(obs.emergency_urgency[2], obs.emergency_urgency[3])
140
  cur = obs.current_phase
141
- ns_q = obs.queue_lengths[0] + obs.queue_lengths[1]
142
- ew_q = obs.queue_lengths[2] + obs.queue_lengths[3]
143
-
144
- # 1. Collision risk — switch to whichever direction has more vehicles
145
- if _collision_risk(obs):
146
- if cur in (0, 3):
147
- if ew_q > ns_q:
148
- return 1, "collision-risk-rotate-EW"
149
- return 0, "collision-risk-hold-NS"
150
- else:
151
- if ns_q > ew_q:
152
- return 0, "collision-risk-rotate-NS"
153
- return 1, "collision-risk-hold-EW"
154
 
155
- # 2. Critical emergency (urgency ≥ 8)
156
  if ns_em > 0 and ns_urg >= 8 and ew_em > 0 and ew_urg >= 8:
157
- return 2, "ALL_RED-dual-critical"
158
  if ns_em > 0 and ns_urg >= 8:
159
- return 0, f"critical-NS-urgency={ns_urg}"
160
  if ew_em > 0 and ew_urg >= 8:
161
- return 1, f"critical-EW-urgency={ew_urg}"
162
 
163
- # 3. Pre-emptive: urgency 6-7 — clear NOW, penalty is already 6^1.5×0.5=11.6/step
164
- # Skip if current direction is already serving it
165
- if ns_em > 0 and ns_urg >= 6:
166
- if cur in (0, 3):
167
- return 0, f"preemptive-NS-hold(urg={ns_urg})"
168
- if ew_em == 0 or ns_urg >= ew_urg:
169
- return 0, f"preemptive-NS-switch(urg={ns_urg})"
170
- if ew_em > 0 and ew_urg >= 6:
171
- if cur in (1, 4):
172
- return 1, f"preemptive-EW-hold(urg={ew_urg})"
173
- if ns_em == 0 or ew_urg >= ns_urg:
174
- return 1, f"preemptive-EW-switch(urg={ew_urg})"
175
-
176
- # 4. Moderate emergency (urgency 5)
177
  if ns_em > 0 and ns_urg >= 5:
178
  if ew_em == 0 or ns_urg >= ew_urg:
179
- return 0, f"moderate-NS(urg={ns_urg})"
180
  if ew_em > 0 and ew_urg >= 5:
181
- return 1, f"moderate-EW(urg={ew_urg})"
182
 
183
- # 5. Hysteresis
184
- hold = _dynamic_hold_time(obs, task)
185
- ratio = SWITCH_RATIO_BY_TASK.get(task, 1.3)
186
- if obs.time_in_phase < hold:
187
- if cur in (0, 3): return 0, f"hysteresis-NS(held={obs.time_in_phase}<{hold})"
188
- if cur in (1, 4): return 1, f"hysteresis-EW(held={obs.time_in_phase}<{hold})"
189
 
190
- # 6. Queue pressure
191
  ns_p, ew_p = _compute_pressures(obs)
192
- if ns_p > ew_p * ratio:
193
- return 0, f"pressure-NS({ns_p:.1f}>{ew_p:.1f}×{ratio})"
194
- if ew_p > ns_p * ratio:
195
- return 1, f"pressure-EW({ew_p:.1f}>{ns_p:.1f}×{ratio})"
196
-
197
- # 7. Hold current
198
- if cur in (0, 3): return 0, "hold-NS"
199
- if cur in (1, 4): return 1, "hold-EW"
200
- return 0, "default-NS"
201
-
202
-
203
- def _should_skip_llm(obs: TrafficObservation, heuristic_phase: int, reason: str, step: int) -> bool:
204
- """
205
- Skip the LLM call when the decision is mathematically obvious OR outside the LLM cadence.
206
- LLM is only called every LLM_CALL_EVERY steps AND only for genuinely ambiguous pressure cases.
207
- """
208
- # Rate-limit: only consider LLM every N steps
209
- if step % LLM_CALL_EVERY != 0:
210
- return True
211
- # Always skip for time-critical or clear-cut decisions
212
- if "collision-risk" in reason:
213
- return True
214
- if "critical" in reason or "preemptive" in reason:
215
- return True
216
- if "hysteresis" in reason:
217
- return True
218
- if "hold" in reason:
219
- return True
220
- # Skip when pressure ratio is clear (> 2×) — heuristic is strictly better here
221
- ns_p, ew_p = _compute_pressures(obs)
222
- max_p = max(ns_p, ew_p, 0.01)
223
- min_p = min(ns_p, ew_p, 0.01)
224
- if max_p / min_p > 2.0:
225
- return True
226
- return False
227
 
228
 
229
  # ---------------------------------------------------------------------------
230
- # Live grade projection
231
  # ---------------------------------------------------------------------------
232
 
233
  def _project_score(task: str, state: Optional[TrafficState], step: int) -> str:
234
  if state is None or step == 0:
235
  return "(no data yet)"
236
-
237
  s = state
238
  steps = max(s.step_count, 1)
239
-
240
- throughput_per_step = s.total_vehicles_passed / steps
241
- em_rate = s.total_emergency_passed / steps
242
- avg_wait = s.total_waiting_time / steps
243
 
244
  if task == "basic_flow":
245
- tput = min(throughput_per_step / 1.8, 1.0)
246
- eff = 1.0 / (1.0 + avg_wait * 0.1)
247
- sw = s.total_phase_changes / steps
248
- stab = max(0.0, 0.05 * (1.0 - min(sw * 4, 1.0)))
249
- proj = tput * 0.6 + eff * 0.4 + stab
250
- gap = max(0.0, 1.8 - throughput_per_step)
251
- return (
252
- f"projected={proj:.3f} | "
253
- f"throughput={tput:.2f}(×0.6, {throughput_per_step:.2f}veh/step, need +{gap:.2f}) "
254
- f"eff={eff:.2f}(×0.4) stab={stab:.3f}(switch={sw:.2f}/step, want<0.25)"
255
- )
256
 
257
  if task == "emergency_priority":
258
- tput = min(throughput_per_step / 1.5, 1.0)
259
- em_score = min(em_rate / (1.0 / 20.0), 1.0)
260
  if s.total_emergency_passed > 0:
261
- delay = max(0.0, 1.0 - (s.total_emergency_delay / s.total_emergency_passed) / 12.0)
262
- avg_d = s.total_emergency_delay / s.total_emergency_passed
263
  else:
264
- delay = 0.5
265
- avg_d = float("inf")
266
- eff = 1.0 / (1.0 + avg_wait * 0.05)
267
- proj = tput * 0.30 + em_score * 0.35 + delay * 0.20 + eff * 0.15
268
- return (
269
- f"projected={proj:.3f} | "
270
- f"em_rate={em_score:.2f}(×0.35, need 1em/20steps) "
271
- f"delay={delay:.2f}(×0.20, avg={avg_d:.1f}steps, want<3) "
272
- f"tput={tput:.2f}(×0.30) eff={eff:.2f}(×0.15)"
273
- )
274
 
275
  if task == "dynamic_scenarios":
276
- tput = min(throughput_per_step / 2.0, 1.0)
277
- em_score = min(em_rate / (1.0 / 15.0), 1.0)
278
  if s.total_emergency_passed > 0:
279
- delay = max(0.0, 1.0 - (s.total_emergency_delay / s.total_emergency_passed) / 5.0)
280
- avg_d = s.total_emergency_delay / s.total_emergency_passed
281
  else:
282
- delay = 0.0
283
- avg_d = float("inf")
284
- eff = 1.0 / (1.0 + avg_wait * 0.08)
285
- adapt = 1.0 / (1.0 + (s.total_phase_changes / steps) * 0.5)
286
- proj = tput * 0.25 + em_score * 0.30 + delay * 0.20 + eff * 0.15 + adapt * 0.10
287
- return (
288
- f"projected={proj:.3f} | "
289
- f"em_rate={em_score:.2f}(×0.30) delay={delay:.2f}(×0.20,avg={avg_d:.1f}) "
290
- f"tput={tput:.2f}(×0.25) eff={eff:.2f}(×0.15) adapt={adapt:.2f}(×0.10)"
291
- )
292
 
293
  return "(unknown task)"
294
 
295
 
296
  # ---------------------------------------------------------------------------
297
- # System prompts (task-specific)
298
  # ---------------------------------------------------------------------------
299
 
300
- _SYSTEM_BASE = """You are an expert Autonomous Traffic Signal Controller for a 4-way intersection.
301
 
302
- PHASES: 0=NS_GREEN 1=EW_GREEN 2=ALL_RED
303
- FLOW RATES: NS_GREEN clears ~3 NS vehicles/step, EW_GREEN clears ~3 EW vehicles/step, ALL_RED clears 0.
304
 
305
  REWARD PER STEP:
306
  +0.30 × regular vehicles cleared
307
  +12.0 × emergency vehicles cleared
308
- -(urgency^1.5)×0.5 per WAITING emergency vehicle (compounds EVERY step it waits!)
309
- urgency=5 → 5.59/step, urgency=7 → 9.26/step, urgency=8 → 11.31/step
310
  -0.08 × total vehicles waiting
311
- -0.5 to -2.0 for unnecessary phase switch (proportional to empty-queue ratio)
312
- +0.05 stability bonus when traffic flows without switching
313
- -200 for gridlock collision (episode ends immediately!)
314
-
315
- SWITCHING COST vs BENEFIT:
316
- Never switch to an empty direction (full -2.0 penalty, zero gain).
317
- Each unnecessary switch also hurts stability/adaptability scores.
318
- A switch is justified ONLY when:
319
- (a) emergency vehicle in new direction, OR
320
- (b) new direction pressure > current direction pressure × task_ratio
321
-
322
- TASK-SPECIFIC GRADING:"""
323
-
324
- _SYSTEM_TASK_HINTS = {
325
- "basic_flow": """
326
- basic_flow weights: throughput×0.60 efficiency×0.40 stability_bonus
327
- TARGET: 1.8 vehicles/step throughput. Switch rate < 0.25/step for stability bonus.
328
- STRATEGY: Hold green phases 5-8 steps. Only switch when NS/EW queue imbalance > 50%.
329
- DO NOT switch to a direction with 0 vehicles — full penalty, zero reward.""",
330
-
331
- "emergency_priority": """
332
- emergency_priority weights: em_rate×0.35 throughput×0.30 delay×0.20 efficiency×0.15
333
- TARGET: Clear 1 emergency per 20 steps. Keep avg emergency delay < 3 steps.
334
- STRATEGY: Pre-clear any urgency≥6 direction IMMEDIATELY — at urgency=6, cost is 11.7/step.
335
- Emergency waiting one extra step costs more than 30 regular vehicles cleared.""",
336
-
337
- "dynamic_scenarios": """
338
- dynamic_scenarios weights: em_rate×0.30 throughput×0.25 delay×0.20 efficiency×0.15 adaptability×0.10
339
- TARGET: 2.0 vehicles/step throughput + clear all emergencies fast. Zero collisions.
340
- STRATEGY: Balance throughput and emergency response. Watch for surge traffic (queue growth > +3/step).
341
- Adaptability penalises OVER-switching — switch only when needed, not on impulse.""",
342
- }
343
-
344
- _SYSTEM_SUFFIX = """
345
-
346
- DECISION RULES (strictly in order):
347
- 1. Total queued > 28 AND held > 14 steps → rotate to larger queue (collision prevention!)
348
- 2. Any urgency ≥ 8 emergency → switch to that direction IMMEDIATELY
349
- 3. Any urgency ≥ 6 emergency in other direction → switch to clear before escalation
350
- 4. Hold current phase until dynamic hold time (varies by queue depth)
351
- 5. Switch only when other direction pressure > current × task_ratio
352
- 6. ALL_RED ONLY when BOTH directions have critical emergencies simultaneously
353
 
354
- Think step by step about (a) emergencies, (b) collision risk, (c) throughput/score impact.
355
- Output ONLY valid JSON on the last line: {"light_phase": 0}"""
 
 
356
 
 
 
 
 
 
 
 
 
357
 
358
- def _get_system_prompt(task: str) -> str:
359
- hint = _SYSTEM_TASK_HINTS.get(task, "")
360
- return _SYSTEM_BASE + hint + _SYSTEM_SUFFIX
361
 
362
 
363
- # ---------------------------------------------------------------------------
364
- # LLM prompt builder
365
- # ---------------------------------------------------------------------------
366
-
367
  def _build_prompt(
368
  obs: TrafficObservation,
369
  step: int,
370
  task: str,
371
  history: Deque[str],
372
  heuristic: int,
373
- heuristic_reason: str,
374
- score_projection: str,
375
  ) -> str:
376
  ns_p, ew_p = _compute_pressures(obs)
377
  ns_q = obs.queue_lengths[0] + obs.queue_lengths[1]
@@ -382,36 +211,31 @@ def _build_prompt(
382
  ew_urg = max(obs.emergency_urgency[2], obs.emergency_urgency[3])
383
  total_q = sum(obs.queue_lengths)
384
 
385
- phase_name = {0: "NS_GREEN", 1: "EW_GREEN", 2: "ALL_RED", 3: "NS_YELLOW", 4: "EW_YELLOW"}
386
- hint_name = {0: "NS_GREEN (0)", 1: "EW_GREEN (1)", 2: "ALL_RED (2)"}
387
- trend_str = f"[{obs.queue_trend[0]:+d},{obs.queue_trend[1]:+d},{obs.queue_trend[2]:+d},{obs.queue_trend[3]:+d}]"
388
-
389
- # Emergency penalty cost — helps LLM quantify urgency
390
- ns_em_cost = f"{ns_em * (max(ns_urg,1)**1.5)*0.5:.1f}/step" if ns_em > 0 else "none"
391
- ew_em_cost = f"{ew_em * (max(ew_urg,1)**1.5)*0.5:.1f}/step" if ew_em > 0 else "none"
392
 
393
- collision_warn = ""
394
- if _collision_risk(obs):
395
- collision_warn = f"\n COLLISION RISK: {total_q} vehicles queued, held {obs.time_in_phase} steps!"
396
 
397
- history_str = "\n".join(history) if history else " (episode start)"
398
 
399
  return (
400
- f"TASK: {task} | Step {step}\n"
401
- f"Phase: {phase_name.get(obs.current_phase, '?')} held {obs.time_in_phase} steps{collision_warn}\n"
402
  f"\n"
403
- f"CURRENT STATE:\n"
404
- f" NS: {ns_q} regular + {ns_em} emergency(urgency={ns_urg}, cost={ns_em_cost}) pressure={ns_p:.1f}\n"
405
- f" EW: {ew_q} regular + {ew_em} emergency(urgency={ew_urg}, cost={ew_em_cost}) pressure={ew_p:.1f}\n"
406
- f" Total queued: {total_q} Trend [N,S,E,W]: {trend_str}\n"
407
- f" Avg wait: {obs.avg_wait_time:.1f} steps | Collision flag: {obs.collision}\n"
408
  f"\n"
409
- f"LIVE SCORE:\n {score_projection}\n"
410
  f"\n"
411
- f"RECENT STEPS:\n{history_str}\n"
412
  f"\n"
413
- f"Heuristic says: {hint_name.get(heuristic, str(heuristic))} ({heuristic_reason})\n"
414
- f"Reason through, then output JSON on the last line."
415
  )
416
 
417
 
@@ -420,10 +244,8 @@ def _build_prompt(
420
  # ---------------------------------------------------------------------------
421
 
422
  def _parse_phase(raw: str) -> Optional[int]:
423
- """Extract phase from LLM chain-of-thought output (JSON on last line)."""
424
  import re
425
- lines = [l.strip() for l in raw.split("\n") if l.strip()]
426
- for line in reversed(lines):
427
  try:
428
  data = json.loads(line)
429
  p = int(data.get("light_phase", data.get("phase", -1)))
@@ -432,11 +254,9 @@ def _parse_phase(raw: str) -> Optional[int]:
432
  except Exception:
433
  pass
434
  m = re.search(r'"light_phase"\s*:\s*([012])', raw)
435
- if m:
436
- return int(m.group(1))
437
  m = re.search(r'\b([012])\b', raw)
438
- if m:
439
- return int(m.group(1))
440
  return None
441
 
442
 
@@ -445,7 +265,7 @@ def _sanitize(s: str) -> str:
445
 
446
 
447
  # ---------------------------------------------------------------------------
448
- # Action selection
449
  # ---------------------------------------------------------------------------
450
 
451
  def get_action(
@@ -457,31 +277,23 @@ def get_action(
457
  state: Optional[TrafficState],
458
  force_heuristic: bool,
459
  ) -> Tuple[TrafficAction, str]:
460
- """
461
- Returns (action, source) where source is "heuristic", "llm", or "fallback".
462
- Uses LLM only for ambiguous cases at the LLM cadence; pure heuristic otherwise.
463
- force_heuristic=True when task time budget is nearly exhausted.
464
- """
465
- heuristic, reason = _heuristic_phase(obs, task)
466
-
467
- # Fast path — skip LLM (obvious decision, wrong cadence, or budget exhausted)
468
- if force_heuristic or _should_skip_llm(obs, heuristic, reason, step):
469
- return TrafficAction(light_phase=heuristic), f"heuristic({reason})"
470
-
471
- # Ambiguous case — call LLM with a hard per-call timeout
472
  score_proj = _project_score(task, state, step)
473
  try:
474
  resp = client.chat.completions.create(
475
  model=MODEL_NAME,
476
  messages=[
477
- {"role": "system", "content": _get_system_prompt(task)},
478
- {"role": "user", "content": _build_prompt(
479
- obs, step, task, history, heuristic, reason, score_proj
480
- )},
481
  ],
482
  temperature=TEMPERATURE,
483
  max_tokens=MAX_TOKENS,
484
- timeout=LLM_TIMEOUT_S,
485
  )
486
  raw = resp.choices[0].message.content.strip()
487
  phase = _parse_phase(raw)
@@ -490,8 +302,7 @@ def get_action(
490
  except Exception:
491
  pass
492
 
493
- # Fallback to heuristic if LLM fails or times out
494
- return TrafficAction(light_phase=heuristic), f"fallback({reason})"
495
 
496
 
497
  # ---------------------------------------------------------------------------
@@ -519,16 +330,15 @@ def _wait_for_server(url: str, timeout: int = 60) -> None:
519
  def run_task(task: str, client: OpenAI) -> dict:
520
  print(f'[START] task={task} env=traffic_control model={MODEL_NAME}', flush=True)
521
 
522
- rewards: List[float] = []
523
- history: Deque[str] = deque(maxlen=8)
524
- step = 0
525
- last_error: Optional[str] = None
526
- done = False
527
- state: Optional[TrafficState] = None
528
- llm_calls = 0
529
- heur_calls = 0
530
- task_start_time = time.time()
531
- budget_s = TASK_BUDGET_S.get(task, 600)
532
 
533
  try:
534
  with TrafficControlEnv(base_url=SERVER_URL).sync() as env:
@@ -539,27 +349,22 @@ def run_task(task: str, client: OpenAI) -> dict:
539
  while not done:
540
  step += 1
541
 
542
- # Refresh cumulative state every 5 steps for score projection
543
- if step % 5 == 1:
544
  try:
545
  state = env.state()
546
  except Exception:
547
  pass
548
 
549
- # Switch to pure heuristic if we're within 60s of the task budget
550
- elapsed = time.time() - task_start_time
551
- force_heuristic = elapsed > budget_s - 60
552
 
553
  action, source = get_action(
554
- client, obs, step, task, history, state,
555
- force_heuristic,
556
  )
557
- action_str = f"light_phase={action.light_phase}"
558
-
559
- if source.startswith("llm"):
560
  llm_calls += 1
561
- else:
562
- heur_calls += 1
563
 
564
  try:
565
  result = env.step(action)
@@ -569,18 +374,20 @@ def run_task(task: str, client: OpenAI) -> dict:
569
  done = result.done
570
  last_error = None
571
 
572
- phase_name = {0: "NS", 1: "EW", 2: "AR", 3: "NSy", 4: "EWy"}
573
  ns_urg = max(obs.emergency_urgency[0], obs.emergency_urgency[1])
574
  ew_urg = max(obs.emergency_urgency[2], obs.emergency_urgency[3])
575
- em_info = ""
576
  if any(q > 0 for q in obs.emergency_queue):
577
- em_info = f" EM[{obs.emergency_queue[0]+obs.emergency_queue[1]}u{ns_urg}|{obs.emergency_queue[2]+obs.emergency_queue[3]}u{ew_urg}]"
 
 
578
  history.append(
579
- f" s{step}: {source[:4]}→{action.light_phase}"
580
  f" clr={obs.vehicles_passed}r+{obs.emergency_passed}em"
581
  f" r={reward_val:+.1f}"
582
- f" ph={phase_name.get(obs.current_phase, '?')}"
583
- f" q={list(obs.queue_lengths)}{em_info}"
584
  )
585
  except Exception as exc:
586
  reward_val = 0.0
@@ -607,10 +414,11 @@ def run_task(task: str, client: OpenAI) -> dict:
607
  max_possible = step * 10.0
608
  score = min(0.999, max(0.001, total_reward / max_possible)) if max_possible > 0 else 0.001
609
 
 
610
  print(
611
  f'[END] success={str(success).lower()} steps={step} '
612
  f'score={score:.3f} rewards={rewards_str} '
613
- f'llm_calls={llm_calls} heuristic_calls={heur_calls}',
614
  flush=True,
615
  )
616
 
@@ -629,8 +437,7 @@ def main():
629
  api_key=API_KEY,
630
  )
631
 
632
- tasks = ["basic_flow", "emergency_priority", "dynamic_scenarios"]
633
- for task in tasks:
634
  run_task(task, client)
635
 
636
 
 
4
  import sys
5
  import json
6
  import time
 
7
  import urllib.request
8
  from collections import deque
9
  from typing import Deque, List, Optional, Tuple
10
 
11
+ _HERE = os.path.dirname(os.path.abspath(__file__))
 
12
  _PARENT = os.path.dirname(_HERE)
13
  for _p in (_HERE, _PARENT):
14
  if _p not in sys.path:
 
24
  from models import TrafficAction, TrafficObservation, TrafficState # type: ignore
25
 
26
  # ---------------------------------------------------------------------------
27
+ # Config — injected by hackathon validator
28
  # ---------------------------------------------------------------------------
29
  API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
30
  API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
31
  MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
32
  SERVER_URL = os.getenv("SERVER_URL", "http://localhost:7860")
33
  SEED = 42
34
+ MAX_TOKENS = 120 # short CoT + JSON — keeps latency low
35
+ TEMPERATURE = 0.0
36
+ LLM_TIMEOUT = 6 # per-call timeout in seconds
37
+
38
+ # Per-task wall-clock budget. Once elapsed > budget - 60s, force pure heuristic
39
+ # so the episode always finishes inside the budget.
40
+ # Total budget: 330+480+630 = 1440s = 24 min — well under the 30-min kill limit.
41
+ TASK_BUDGET_S = {
42
+ "basic_flow": 330,
43
+ "emergency_priority": 480,
44
+ "dynamic_scenarios": 630,
45
  }
46
 
 
47
  TASK_MAX_STEPS = {"basic_flow": 200, "emergency_priority": 300, "dynamic_scenarios": 400}
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  # ---------------------------------------------------------------------------
51
+ # Heuristic (fallback only used when budget is nearly exhausted or LLM fails)
52
  # ---------------------------------------------------------------------------
53
 
54
  def _em_pressure(count: int, urgency: int) -> float:
 
55
  return count * (max(urgency, 1) ** 1.5) * 0.5 if count > 0 else 0.0
56
 
 
57
  def _dir_pressure(queue: int, em_count: int, urgency: int) -> float:
 
58
  return queue * 0.30 + _em_pressure(em_count, urgency) * 4.0
59
 
 
60
  def _compute_pressures(obs: TrafficObservation) -> Tuple[float, float]:
61
+ ns = _dir_pressure(
 
62
  obs.queue_lengths[0] + obs.queue_lengths[1],
63
  obs.emergency_queue[0] + obs.emergency_queue[1],
64
  max(obs.emergency_urgency[0], obs.emergency_urgency[1]),
65
  )
66
+ ew = _dir_pressure(
67
  obs.queue_lengths[2] + obs.queue_lengths[3],
68
  obs.emergency_queue[2] + obs.emergency_queue[3],
69
  max(obs.emergency_urgency[2], obs.emergency_urgency[3]),
70
  )
71
+ return ns, ew
72
+
73
+ def _heuristic_phase(obs: TrafficObservation, task: str) -> int:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  ns_em = obs.emergency_queue[0] + obs.emergency_queue[1]
75
  ew_em = obs.emergency_queue[2] + obs.emergency_queue[3]
76
  ns_urg = max(obs.emergency_urgency[0], obs.emergency_urgency[1])
77
  ew_urg = max(obs.emergency_urgency[2], obs.emergency_urgency[3])
78
  cur = obs.current_phase
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ # Critical emergency
81
  if ns_em > 0 and ns_urg >= 8 and ew_em > 0 and ew_urg >= 8:
82
+ return 2
83
  if ns_em > 0 and ns_urg >= 8:
84
+ return 0
85
  if ew_em > 0 and ew_urg >= 8:
86
+ return 1
87
 
88
+ # Moderate emergency
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  if ns_em > 0 and ns_urg >= 5:
90
  if ew_em == 0 or ns_urg >= ew_urg:
91
+ return 0
92
  if ew_em > 0 and ew_urg >= 5:
93
+ return 1
94
 
95
+ # Hysteresis
96
+ min_hold = 6 if task == "basic_flow" else 3
97
+ if obs.time_in_phase < min_hold:
98
+ if cur in (0, 3): return 0
99
+ if cur in (1, 4): return 1
 
100
 
101
+ # Pressure
102
  ns_p, ew_p = _compute_pressures(obs)
103
+ ratio = 1.5 if task == "basic_flow" else 1.2
104
+ if ns_p > ew_p * ratio: return 0
105
+ if ew_p > ns_p * ratio: return 1
106
+
107
+ # Hold current
108
+ if cur in (0, 3): return 0
109
+ if cur in (1, 4): return 1
110
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
 
113
  # ---------------------------------------------------------------------------
114
+ # Live score projection
115
  # ---------------------------------------------------------------------------
116
 
117
  def _project_score(task: str, state: Optional[TrafficState], step: int) -> str:
118
  if state is None or step == 0:
119
  return "(no data yet)"
 
120
  s = state
121
  steps = max(s.step_count, 1)
122
+ tps = s.total_vehicles_passed / steps
123
+ er = s.total_emergency_passed / steps
124
+ aw = s.total_waiting_time / steps
 
125
 
126
  if task == "basic_flow":
127
+ tput = min(tps / 1.8, 1.0)
128
+ eff = 1.0 / (1.0 + aw * 0.1)
129
+ sw = s.total_phase_changes / steps
130
+ stab = max(0.0, 0.05 * (1.0 - min(sw * 4, 1.0)))
131
+ proj = tput * 0.6 + eff * 0.4 + stab
132
+ return f"projected={proj:.3f} tput={tput:.2f}(×0.6,{tps:.2f}v/s,need1.8) eff={eff:.2f}(×0.4) stab={stab:.3f}(sw={sw:.2f}/s)"
 
 
 
 
 
133
 
134
  if task == "emergency_priority":
135
+ tput = min(tps / 1.5, 1.0)
136
+ ems = min(er / (1.0/20.0), 1.0)
137
  if s.total_emergency_passed > 0:
138
+ d = max(0.0, 1.0 - (s.total_emergency_delay / s.total_emergency_passed) / 12.0)
139
+ avgd = s.total_emergency_delay / s.total_emergency_passed
140
  else:
141
+ d, avgd = 0.5, float("inf")
142
+ eff = 1.0 / (1.0 + aw * 0.05)
143
+ proj = tput*0.30 + ems*0.35 + d*0.20 + eff*0.15
144
+ return f"projected={proj:.3f} em={ems:.2f}(×0.35) delay={d:.2f}(×0.20,avg={avgd:.1f}) tput={tput:.2f}(×0.30)"
 
 
 
 
 
 
145
 
146
  if task == "dynamic_scenarios":
147
+ tput = min(tps / 2.0, 1.0)
148
+ ems = min(er / (1.0/15.0), 1.0)
149
  if s.total_emergency_passed > 0:
150
+ d = max(0.0, 1.0 - (s.total_emergency_delay / s.total_emergency_passed) / 5.0)
151
+ avgd = s.total_emergency_delay / s.total_emergency_passed
152
  else:
153
+ d, avgd = 0.0, float("inf")
154
+ eff = 1.0 / (1.0 + aw * 0.08)
155
+ ada = 1.0 / (1.0 + (s.total_phase_changes / steps) * 0.5)
156
+ proj = tput*0.25 + ems*0.30 + d*0.20 + eff*0.15 + ada*0.10
157
+ return f"projected={proj:.3f} em={ems:.2f}(×0.30) delay={d:.2f}(avg={avgd:.1f}) tput={tput:.2f} eff={eff:.2f} ada={ada:.2f}"
 
 
 
 
 
158
 
159
  return "(unknown task)"
160
 
161
 
162
  # ---------------------------------------------------------------------------
163
+ # System prompt
164
  # ---------------------------------------------------------------------------
165
 
166
+ SYSTEM_PROMPT = """You are an expert Autonomous Traffic Signal Controller for a 4-way intersection.
167
 
168
+ PHASES: 0=NS_GREEN (North-South green) 1=EW_GREEN (East-West green) 2=ALL_RED (emergency clearance)
169
+ FLOW: each GREEN phase clears ~3 vehicles/step in that direction. ALL_RED clears 0.
170
 
171
  REWARD PER STEP:
172
  +0.30 × regular vehicles cleared
173
  +12.0 × emergency vehicles cleared
174
+ -(urgency^1.5)×0.5 per WAITING emergency (every step it waits — urgency=6→7.4/step, urgency=8→11.3/step)
 
175
  -0.08 × total vehicles waiting
176
+ -0.5 to -2.0 unnecessary phase switch (proportional to empty-queue ratio)
177
+ +0.05 stability bonus (no switch this step)
178
+ -200 gridlock collision (instant episode end!)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ GRADING WEIGHTS:
181
+ basic_flow: throughput×0.60 efficiency×0.40 (+stability bonus)
182
+ emergency_priority: em_rate×0.35 throughput×0.30 delay×0.20 efficiency×0.15
183
+ dynamic_scenarios: em_rate×0.30 throughput×0.25 delay×0.20 efficiency×0.15 adaptability×0.10
184
 
185
+ RULES:
186
+ 1. Any urgency≥8 emergency → switch to that direction NOW (cost=11.3/step if you wait)
187
+ 2. Any urgency≥6 emergency → clear before it escalates (cost=7.4/step)
188
+ 3. basic_flow: hold each phase ≥6 steps; switch only when other direction queue is 50%+ bigger
189
+ 4. emergency tasks: react fast, hold ≥3 steps minimum
190
+ 5. ALL_RED only when BOTH directions have simultaneous critical emergencies
191
+ 6. Never switch to an empty direction (full -2.0 penalty, zero gain)
192
+ 7. Total queue >28 + held >14 steps → rotate to prevent -200 gridlock collision
193
 
194
+ Think step-by-step then output ONLY valid JSON on the last line: {"light_phase": 0}"""
 
 
195
 
196
 
 
 
 
 
197
  def _build_prompt(
198
  obs: TrafficObservation,
199
  step: int,
200
  task: str,
201
  history: Deque[str],
202
  heuristic: int,
203
+ score_proj: str,
 
204
  ) -> str:
205
  ns_p, ew_p = _compute_pressures(obs)
206
  ns_q = obs.queue_lengths[0] + obs.queue_lengths[1]
 
211
  ew_urg = max(obs.emergency_urgency[2], obs.emergency_urgency[3])
212
  total_q = sum(obs.queue_lengths)
213
 
214
+ pname = {0:"NS_GREEN",1:"EW_GREEN",2:"ALL_RED",3:"NS_YELLOW",4:"EW_YELLOW"}
215
+ hname = {0:"NS_GREEN(0)",1:"EW_GREEN(1)",2:"ALL_RED(2)"}
216
+ trend = f"[{obs.queue_trend[0]:+d},{obs.queue_trend[1]:+d},{obs.queue_trend[2]:+d},{obs.queue_trend[3]:+d}]"
 
 
 
 
217
 
218
+ ns_cost = f"{ns_em*(max(ns_urg,1)**1.5)*0.5:.1f}/step" if ns_em > 0 else "none"
219
+ ew_cost = f"{ew_em*(max(ew_urg,1)**1.5)*0.5:.1f}/step" if ew_em > 0 else "none"
220
+ collision_warn = f"\n *** COLLISION RISK: {total_q} queued, held {obs.time_in_phase} steps! ***" if total_q > 28 and obs.time_in_phase > 14 else ""
221
 
222
+ hist_str = "\n".join(history) if history else " (start)"
223
 
224
  return (
225
+ f"TASK: {task} Step {step}\n"
226
+ f"Phase: {pname.get(obs.current_phase,'?')} held {obs.time_in_phase} steps{collision_warn}\n"
227
  f"\n"
228
+ f"STATE:\n"
229
+ f" NS: {ns_q} vehicles + {ns_em} emergency(urgency={ns_urg}, cost={ns_cost}) pressure={ns_p:.1f}\n"
230
+ f" EW: {ew_q} vehicles + {ew_em} emergency(urgency={ew_urg}, cost={ew_cost}) pressure={ew_p:.1f}\n"
231
+ f" Total queued: {total_q} Trend[N,S,E,W]: {trend} Avg wait: {obs.avg_wait_time:.1f}s\n"
 
232
  f"\n"
233
+ f"SCORE: {score_proj}\n"
234
  f"\n"
235
+ f"HISTORY:\n{hist_str}\n"
236
  f"\n"
237
+ f"Heuristic recommends: {hname.get(heuristic, str(heuristic))}\n"
238
+ f"Reason through the decision, then output JSON on the last line."
239
  )
240
 
241
 
 
244
  # ---------------------------------------------------------------------------
245
 
246
  def _parse_phase(raw: str) -> Optional[int]:
 
247
  import re
248
+ for line in reversed([l.strip() for l in raw.split("\n") if l.strip()]):
 
249
  try:
250
  data = json.loads(line)
251
  p = int(data.get("light_phase", data.get("phase", -1)))
 
254
  except Exception:
255
  pass
256
  m = re.search(r'"light_phase"\s*:\s*([012])', raw)
257
+ if m: return int(m.group(1))
 
258
  m = re.search(r'\b([012])\b', raw)
259
+ if m: return int(m.group(1))
 
260
  return None
261
 
262
 
 
265
 
266
 
267
  # ---------------------------------------------------------------------------
268
+ # Action: LLM every step, heuristic fallback
269
  # ---------------------------------------------------------------------------
270
 
271
  def get_action(
 
277
  state: Optional[TrafficState],
278
  force_heuristic: bool,
279
  ) -> Tuple[TrafficAction, str]:
280
+ heuristic = _heuristic_phase(obs, task)
281
+
282
+ # Pure heuristic when time budget is nearly exhausted
283
+ if force_heuristic:
284
+ return TrafficAction(light_phase=heuristic), "heuristic(budget)"
285
+
 
 
 
 
 
 
286
  score_proj = _project_score(task, state, step)
287
  try:
288
  resp = client.chat.completions.create(
289
  model=MODEL_NAME,
290
  messages=[
291
+ {"role": "system", "content": SYSTEM_PROMPT},
292
+ {"role": "user", "content": _build_prompt(obs, step, task, history, heuristic, score_proj)},
 
 
293
  ],
294
  temperature=TEMPERATURE,
295
  max_tokens=MAX_TOKENS,
296
+ timeout=LLM_TIMEOUT,
297
  )
298
  raw = resp.choices[0].message.content.strip()
299
  phase = _parse_phase(raw)
 
302
  except Exception:
303
  pass
304
 
305
+ return TrafficAction(light_phase=heuristic), "heuristic(fallback)"
 
306
 
307
 
308
  # ---------------------------------------------------------------------------
 
330
  def run_task(task: str, client: OpenAI) -> dict:
331
  print(f'[START] task={task} env=traffic_control model={MODEL_NAME}', flush=True)
332
 
333
+ rewards: List[float] = []
334
+ history: Deque[str] = deque(maxlen=6)
335
+ step = 0
336
+ last_error: Optional[str] = None
337
+ done = False
338
+ state: Optional[TrafficState] = None
339
+ llm_calls = 0
340
+ task_start = time.time()
341
+ budget_s = TASK_BUDGET_S.get(task, 600)
 
342
 
343
  try:
344
  with TrafficControlEnv(base_url=SERVER_URL).sync() as env:
 
349
  while not done:
350
  step += 1
351
 
352
+ # Refresh cumulative state every 10 steps
353
+ if step % 10 == 1:
354
  try:
355
  state = env.state()
356
  except Exception:
357
  pass
358
 
359
+ elapsed = time.time() - task_start
360
+ force_heuristic = elapsed > budget_s - 60
 
361
 
362
  action, source = get_action(
363
+ client, obs, step, task, history, state, force_heuristic
 
364
  )
365
+ action_str = f"light_phase={action.light_phase}"
366
+ if source == "llm":
 
367
  llm_calls += 1
 
 
368
 
369
  try:
370
  result = env.step(action)
 
374
  done = result.done
375
  last_error = None
376
 
377
+ pname = {0:"NS",1:"EW",2:"AR",3:"NSy",4:"EWy"}
378
  ns_urg = max(obs.emergency_urgency[0], obs.emergency_urgency[1])
379
  ew_urg = max(obs.emergency_urgency[2], obs.emergency_urgency[3])
380
+ em_str = ""
381
  if any(q > 0 for q in obs.emergency_queue):
382
+ ns_e = obs.emergency_queue[0]+obs.emergency_queue[1]
383
+ ew_e = obs.emergency_queue[2]+obs.emergency_queue[3]
384
+ em_str = f" EM[{ns_e}u{ns_urg}|{ew_e}u{ew_urg}]"
385
  history.append(
386
+ f" s{step}({source[:3]}):→{action.light_phase}"
387
  f" clr={obs.vehicles_passed}r+{obs.emergency_passed}em"
388
  f" r={reward_val:+.1f}"
389
+ f" ph={pname.get(obs.current_phase,'?')}"
390
+ f" q={list(obs.queue_lengths)}{em_str}"
391
  )
392
  except Exception as exc:
393
  reward_val = 0.0
 
414
  max_possible = step * 10.0
415
  score = min(0.999, max(0.001, total_reward / max_possible)) if max_possible > 0 else 0.001
416
 
417
+ elapsed_total = time.time() - task_start
418
  print(
419
  f'[END] success={str(success).lower()} steps={step} '
420
  f'score={score:.3f} rewards={rewards_str} '
421
+ f'llm_calls={llm_calls} elapsed={elapsed_total:.0f}s',
422
  flush=True,
423
  )
424
 
 
437
  api_key=API_KEY,
438
  )
439
 
440
+ for task in ["basic_flow", "emergency_priority", "dynamic_scenarios"]:
 
441
  run_task(task, client)
442
 
443